feat(seo): 사이트맵 안전 서빙·잡 견고화·분할 설정 + 캐시 설정 정합

봇 요청 스레드의 동기 생성을 제거하고 디스크 스트리밍 서빙으로 전환했다.
신선도가 만료되면 생성 잡만 예약하고 기존 세트(stale) 또는 503+Retry-After 로
응답해, 대용량에서 봇 요청이 OOM/타임아웃을 유발하던 경로를 끊었다.

- 잡: ShouldBeUnique(고정 uniqueId)+uniqueFor+retryUntil, timeout 300→1800, backoff
- 컨트롤러: 인덱스 stale/503 분기 + 자식 라우트(.xml/.xml.gz 동일 액션)
- 스케줄 withoutOverlapping, 관리자 재생성 큐 이관
- 설정 4키(분할 기준·압축·stale 서빙·수집기 상한) + 고급탭 Sitemap 캐시 짝
- SitemapManager 응답 메시지 다국어화

설정 저장 테스트가 실제 설정 파일(사이트명/언어 포함)을 덮어쓰던 사전결함을
TestCase 전역 격리로 차단하고, 두 탭이 같은 다국어 키를 공유해 한쪽에 틀린
한도를 안내하던 사전결함을 키 분리로 해소했다.
This commit is contained in:
HeuJung
2026-07-18 11:32:11 +09:00
parent 21f666e4b5
commit 1c99547bc1
71 changed files with 3767 additions and 2472 deletions
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Http\Requests\Admin\SeoCacheClearRequest;
use App\Jobs\GenerateSitemapJob;
use App\Seo\Contracts\SeoCacheManagerInterface;
use App\Seo\SeoCacheStatsService;
use App\Seo\SitemapManager;
@@ -54,7 +55,7 @@ class SeoCacheController extends AdminBaseController
*
* 레이아웃 또는 모듈 지정 시 해당 캐시만, 미지정 시 전체 캐시를 삭제합니다.
*
* @param SeoCacheClearRequest $request 캐시 삭제 요청
* @param SeoCacheClearRequest $request 캐시 삭제 요청
* @return JsonResponse 삭제 결과를 포함한 JSON 응답
*/
public function clearCache(SeoCacheClearRequest $request): JsonResponse
@@ -97,28 +98,25 @@ class SeoCacheController extends AdminBaseController
}
/**
* Sitemap XML 을 즉시 재생성합니다.
* Sitemap XML 재생성을 큐에 예약합니다.
*
* 큐 드라이버와 무관하게 동기 실행되며, 생성 완료 후 last_updated_at 을 갱신합니다.
* 대용량(수백만 URL) 사이트에서 요청 스레드 동기 생성은 메모리/타임아웃 붕괴를 일으키므로
* 큐 잡으로 위임합니다. 응답은 예약 시점의 상태이며, 실제 완료 시각은 이후 갱신됩니다.
*
* @return JsonResponse 재생성 결과 JSON 응답
* @return JsonResponse 예약 결과 JSON 응답
*/
public function regenerateSitemap(): JsonResponse
{
$result = $this->sitemapManager->regenerate();
if ($result['success']) {
return $this->success('seo.sitemap_regenerated', $result['data'] ?? null);
if (! (bool) g7_core_settings('seo.sitemap_enabled', true)) {
return $this->error('seo.sitemap_disabled', 400);
}
$messageKey = match ($result['status']) {
'disabled' => 'seo.sitemap_disabled',
default => 'seo.sitemap_regenerate_failed',
};
GenerateSitemapJob::dispatch();
$statusCode = $result['status'] === 'disabled' ? 400 : 500;
return $this->error($messageKey, $statusCode, $result['message'] ?? null);
return $this->success('seo.sitemap_regenerate_dispatched', [
'status' => 'dispatched',
...$this->sitemapManager->getStatus(),
]);
}
/**
@@ -4,44 +4,92 @@ namespace App\Http\Controllers\Api\Public;
use App\Contracts\Extension\CacheInterface;
use App\Http\Controllers\Controller;
use App\Seo\SeoCacheSettings;
use App\Seo\SitemapGenerator;
use Illuminate\Http\Response;
use App\Jobs\GenerateSitemapJob;
use App\Seo\SitemapFileStore;
use App\Seo\SitemapManager;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* Sitemap XML 컨트롤러
*
* sitemap.xml 요청을 처리하여 캐시된 또는 실시간 생성된
* Sitemap XML을 반환합니다.
* 비공개 디스크에 커밋된 sitemap 인덱스/자식 파일을 스트리밍으로 서빙합니다.
* 봇 요청 스레드에서 sitemap 을 생성하지 않으며(대용량에서 OOM/타임아웃 원인),
* 신선도가 만료되면 생성 잡만 예약하고 기존 세트(stale)를 그대로 내보냅니다.
*/
class SitemapController extends Controller
{
/**
* sitemap.xml을 반환합니다.
*
* 캐시가 있으면 캐시된 XML, 없으면 실시간 생성 후 캐시 저장.
*
* @param SitemapGenerator $generator Sitemap 생성기
* @param CacheInterface $cache 코어 캐시 드라이버
* @return Response Sitemap XML 응답
* 세트가 없어 503 을 반환할 때 봇에 안내할 재시도 간격 (초)
*/
public function index(SitemapGenerator $generator, CacheInterface $cache): Response
private const RETRY_AFTER_SECONDS = 120;
/**
* sitemapindex 를 반환합니다.
*
* 커밋된 세트가 있으면 스트리밍하고, 신선도(메타 캐시)가 만료되었으면
* 생성 잡을 예약합니다. 세트가 전혀 없으면 503 + Retry-After 를 반환합니다.
*
* @param SitemapFileStore $store sitemap 파일 저장소 (읽기측)
* @param CacheInterface $cache 코어 캐시 드라이버
* @return StreamedResponse sitemapindex 스트림
*/
public function index(SitemapFileStore $store, CacheInterface $cache): StreamedResponse
{
$enabled = (bool) g7_core_settings('seo.sitemap_enabled', true);
if (! $enabled) {
$this->assertSitemapEnabled();
$isFresh = $cache->get(SitemapManager::META_CACHE_KEY) !== null;
if ($isFresh) {
$response = $store->indexResponse();
if ($response !== null) {
return $response;
}
}
// 신선도 만료 또는 세트 부재 — 생성은 큐에 맡긴다 (ShouldBeUnique 로 중복 디스패치 방지).
GenerateSitemapJob::dispatch();
$serveStale = (bool) g7_core_settings('seo.sitemap_serve_stale_on_miss', true);
if ($serveStale) {
$response = $store->indexResponse();
if ($response !== null) {
return $response;
}
}
abort(503, __('seo.sitemap_not_ready'), ['Retry-After' => (string) self::RETRY_AFTER_SECONDS]);
}
/**
* 자식 sitemap 을 반환합니다.
*
* gzip 여부는 manifest 가 결정하므로 `.xml` / `.xml.gz` 요청을 같은 액션에서 처리합니다.
*
* @param int $n 자식 번호 (1부터)
* @param SitemapFileStore $store sitemap 파일 저장소 (읽기측)
* @return StreamedResponse 자식 sitemap 스트림
*/
public function child(int $n, SitemapFileStore $store): StreamedResponse
{
$this->assertSitemapEnabled();
$response = $store->childResponse($n);
if ($response === null) {
abort(404);
}
$xml = $cache->get('seo.sitemap');
return $response;
}
if (! $xml) {
$xml = $generator->generate();
// 고급 탭(cache.seo_sitemap_ttl)이 메인, SEO 탭에 별도 지정이 있으면 그것이 우선 (D19)
$cache->put('seo.sitemap', $xml, SeoCacheSettings::sitemapCacheTtl());
/**
* Sitemap 기능이 비활성화되어 있으면 404 로 중단합니다.
*/
private function assertSitemapEnabled(): void
{
if (! (bool) g7_core_settings('seo.sitemap_enabled', true)) {
abort(404);
}
return response($xml, 200, [
'Content-Type' => 'application/xml',
]);
}
}
@@ -220,6 +220,11 @@ class SaveSettingsRequest extends FormRequest
'seo.cache_ttl' => ['nullable', 'integer', 'min:60', 'max:86400'],
'seo.sitemap_enabled' => ['nullable', 'boolean'],
'seo.sitemap_cache_ttl' => ['nullable', 'integer', 'min:3600', 'max:604800'],
// 분할 기준. 상한 50000 은 sitemaps.org 프로토콜 제한이며 SitemapWriter 가 동일 값으로 클램프합니다.
'seo.sitemap_urls_per_file' => ['nullable', 'integer', 'min:1000', 'max:50000'],
'seo.sitemap_gzip' => ['nullable', 'boolean'],
'seo.sitemap_serve_stale_on_miss' => ['nullable', 'boolean'],
'seo.sitemap_max_urls_per_contributor' => ['nullable', 'integer', 'min:0'],
'seo.sitemap_schedule' => ['nullable', 'string', Rule::in(['hourly', 'daily', 'weekly'])],
'seo.sitemap_schedule_time' => ['nullable', 'string', 'regex:/^\d{2}:\d{2}$/'],
'seo.generator_enabled' => ['nullable', 'boolean'],
@@ -242,6 +247,9 @@ class SaveSettingsRequest extends FormRequest
'advanced.stats_cache_ttl' => $this->getAdvancedTabCacheTtlRules($tab),
'advanced.seo_cache_enabled' => $this->getTabRules($tab, 'advanced', 'boolean'),
'advanced.seo_cache_ttl' => $this->getAdvancedTabCacheTtlRules($tab),
// 신규 설정이므로 미전송(기존 클라이언트)을 허용한다 — 미전송/미지정 시 코드 기본값 유지.
// 범위는 SEO 탭의 오버라이드 칸(seo.sitemap_cache_ttl)과 동일하게 맞춘다.
'advanced.seo_sitemap_cache_ttl' => ['nullable', 'integer', 'min:3600', 'max:604800'],
// 디버그 설정 (advanced 탭)
'advanced.debug_mode' => $this->getTabRules($tab, 'advanced', 'boolean'),
@@ -605,6 +613,13 @@ class SaveSettingsRequest extends FormRequest
'seo.sitemap_cache_ttl.integer' => __('validation.settings.sitemap_cache_ttl_integer'),
'seo.sitemap_cache_ttl.min' => __('validation.settings.sitemap_cache_ttl_min'),
'seo.sitemap_cache_ttl.max' => __('validation.settings.sitemap_cache_ttl_max'),
'seo.sitemap_urls_per_file.integer' => __('validation.settings.sitemap_urls_per_file_integer'),
'seo.sitemap_urls_per_file.min' => __('validation.settings.sitemap_urls_per_file_min'),
'seo.sitemap_urls_per_file.max' => __('validation.settings.sitemap_urls_per_file_max'),
'seo.sitemap_gzip.boolean' => __('validation.settings.sitemap_gzip_boolean'),
'seo.sitemap_serve_stale_on_miss.boolean' => __('validation.settings.sitemap_serve_stale_on_miss_boolean'),
'seo.sitemap_max_urls_per_contributor.integer' => __('validation.settings.sitemap_max_urls_per_contributor_integer'),
'seo.sitemap_max_urls_per_contributor.min' => __('validation.settings.sitemap_max_urls_per_contributor_min'),
'seo.sitemap_schedule.in' => __('validation.settings.sitemap_schedule_invalid'),
'seo.sitemap_schedule_time.regex' => __('validation.settings.sitemap_schedule_time_invalid'),
@@ -639,12 +654,15 @@ class SaveSettingsRequest extends FormRequest
'advanced.stats_cache_ttl.integer' => __('validation.settings.stats_cache_ttl_integer'),
'advanced.stats_cache_ttl.min' => __('validation.settings.stats_cache_ttl_min'),
'advanced.stats_cache_ttl.max' => __('validation.settings.stats_cache_ttl_max'),
'advanced.seo_cache_enabled.required' => __('validation.settings.seo_cache_enabled_required'),
'advanced.seo_cache_enabled.boolean' => __('validation.settings.seo_cache_enabled_boolean'),
'advanced.seo_cache_ttl.required' => __('validation.settings.seo_cache_ttl_required'),
'advanced.seo_cache_ttl.integer' => __('validation.settings.seo_cache_ttl_integer'),
'advanced.seo_cache_ttl.min' => __('validation.settings.seo_cache_ttl_min'),
'advanced.seo_cache_ttl.max' => __('validation.settings.seo_cache_ttl_max'),
'advanced.seo_cache_enabled.required' => __('validation.settings.advanced_seo_cache_enabled_required'),
'advanced.seo_cache_enabled.boolean' => __('validation.settings.advanced_seo_cache_enabled_boolean'),
'advanced.seo_cache_ttl.required' => __('validation.settings.advanced_seo_cache_ttl_required'),
'advanced.seo_cache_ttl.integer' => __('validation.settings.advanced_seo_cache_ttl_integer'),
'advanced.seo_cache_ttl.min' => __('validation.settings.advanced_seo_cache_ttl_min'),
'advanced.seo_cache_ttl.max' => __('validation.settings.advanced_seo_cache_ttl_max'),
'advanced.seo_sitemap_cache_ttl.integer' => __('validation.settings.seo_sitemap_cache_ttl_integer'),
'advanced.seo_sitemap_cache_ttl.min' => __('validation.settings.seo_sitemap_cache_ttl_min'),
'advanced.seo_sitemap_cache_ttl.max' => __('validation.settings.seo_sitemap_cache_ttl_max'),
// 디버그 설정
'advanced.debug_mode.required' => __('validation.settings.debug_mode_required'),
+42 -4
View File
@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Seo\SitemapManager;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
@@ -13,10 +14,13 @@ use Illuminate\Support\Facades\Log;
/**
* Sitemap XML 생성 큐 잡
*
* 스케줄러 또는 Artisan 커맨드에서 디스패치되며,
* 스케줄러, Artisan 커맨드, 관리자 수동 재생성, 봇 요청 캐시 미스에서 디스패치되며,
* 실제 생성 로직은 SitemapManager 서비스에 위임합니다.
*
* 대용량(수백만 URL) 생성은 수 분~수십 분이 걸리므로 동시 실행을 막고(ShouldBeUnique)
* 타임아웃/재시도 한계를 넉넉히 잡되, 재시도가 무한히 누적되지 않도록 retryUntil 로 제한합니다.
*/
class GenerateSitemapJob implements ShouldQueue
class GenerateSitemapJob implements ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@@ -26,9 +30,43 @@ class GenerateSitemapJob implements ShouldQueue
public int $tries = 3;
/**
* @var int 타임아웃 (초)
* @var int 타임아웃 (초) — 대용량 스트리밍 생성 대응
*/
public int $timeout = 300;
public int $timeout = 1800;
/**
* @var int 재시도 대기 시간 (초)
*/
public int $backoff = 120;
/**
* @var int 유니크 락 유지 시간 (초) — 잡이 비정상 종료해도 이 시간 후 자동 해제
*/
public int $uniqueFor = 900;
/**
* 유니크 락 식별자를 반환합니다.
*
* 봇 요청 캐시 미스마다 디스패치되어도 큐에는 한 건만 남도록 고정 키를 사용합니다.
*
* @return string 유니크 락 키
*/
public function uniqueId(): string
{
return 'seo-sitemap';
}
/**
* 재시도 마감 시각을 반환합니다.
*
* timeout(1800초) × tries(3) 만큼 재시도가 누적되는 것을 막습니다.
*
* @return \DateTime 재시도 마감 시각
*/
public function retryUntil(): \DateTime
{
return now()->addMinutes(30)->toDateTime();
}
/**
* Sitemap 을 생성하고 캐시에 저장합니다.
+3 -3
View File
@@ -48,7 +48,7 @@ class SitemapManager
return [
'success' => false,
'status' => 'disabled',
'message' => 'Sitemap 생성이 비활성화되어 있습니다.',
'message' => __('seo.sitemap_disabled'),
];
}
@@ -70,7 +70,7 @@ class SitemapManager
$result = [
'success' => true,
'status' => 'updated',
'message' => 'Sitemap 생성이 완료되었습니다.',
'message' => __('seo.sitemap_regenerated'),
'data' => [
'last_updated_at' => $lastUpdatedAt,
'size_bytes' => $meta['size_bytes'],
@@ -91,7 +91,7 @@ class SitemapManager
$result = [
'success' => false,
'status' => 'failed',
'message' => 'Sitemap 생성에 실패했습니다: '.$e->getMessage(),
'message' => __('seo.sitemap_generate_failed', ['error' => $e->getMessage()]),
];
HookManager::doAction('core.seo.sitemap.after_regenerate_failed', $result);
+1 -1
View File
@@ -563,7 +563,7 @@ class SettingsService
// 각 카테고리에 속하는 원본 필드명 목록
// (frontend_key → 원본 키 역변환 후의 키 기준)
$categoryFieldMap = [
'cache' => ['enabled', 'layout_enabled', 'layout_ttl', 'stats_enabled', 'stats_ttl', 'seo_enabled', 'seo_ttl'],
'cache' => ['enabled', 'layout_enabled', 'layout_ttl', 'stats_enabled', 'stats_ttl', 'seo_enabled', 'seo_ttl', 'seo_sitemap_ttl'],
'debug' => ['mode', 'sql_query_log', 'log_level'],
'core_update' => ['github_url', 'github_token'],
'geoip' => ['feature_enabled', 'license_key', 'auto_update_enabled', 'last_updated_at'],
+8
View File
@@ -69,6 +69,10 @@
"cache_ttl": null,
"sitemap_enabled": true,
"sitemap_cache_ttl": null,
"sitemap_urls_per_file": 50000,
"sitemap_gzip": false,
"sitemap_serve_stale_on_miss": true,
"sitemap_max_urls_per_contributor": 0,
"sitemap_schedule": "daily",
"sitemap_schedule_time": "02:00",
"sitemap_last_updated_at": "",
@@ -213,6 +217,10 @@
"cache_ttl": { "type": "integer", "sensitive": false },
"sitemap_enabled": { "type": "boolean", "sensitive": false },
"sitemap_cache_ttl": { "type": "integer", "sensitive": false },
"sitemap_urls_per_file": { "type": "integer", "sensitive": false },
"sitemap_gzip": { "type": "boolean", "sensitive": false },
"sitemap_serve_stale_on_miss": { "type": "boolean", "sensitive": false },
"sitemap_max_urls_per_contributor": { "type": "integer", "sensitive": false },
"sitemap_schedule": { "type": "string", "sensitive": false },
"sitemap_schedule_time": { "type": "string", "sensitive": false },
"sitemap_last_updated_at": { "type": "string", "sensitive": false },
+11 -8
View File
@@ -152,9 +152,8 @@ _단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| last_updated_at | string | `2026-07-08T03:14:49+00:00` | last updated 일시 |
| size_bytes | integer | `25489` | 크기 (바이트) |
| ttl | integer | `86400` | 캐시 유효 시간 (Time To Live, 초). 고급 설정의 `cache.seo_sitemap_ttl` 이 기준값이며, SEO 설정 `seo.sitemap_cache_ttl` 에 값이 지정되어 있으면(null 이 아니면) 그 값이 우선합니다 |
| status | string | `dispatched` | 예약 상태. 재생성 잡이 큐에 등록되었음을 의미합니다 |
| last_updated_at | string\|null | `2026-07-08T03:14:49+00:00` | **예약 시점 기준** 마지막 생성 일시. 이번 재생성의 결과가 아니라 직전 생성 시각이며, 잡 완료 후 갱신됩니다 (아직 한 번도 생성되지 않았으면 null) |
**응답 예시**
@@ -165,11 +164,10 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "Sitemap 재생성이 완료되었습니다.",
"message": "Sitemap 재생성을 시작했습니다. 완료까지 시간이 걸릴 수 있습니다.",
"data": {
"last_updated_at": "2026-07-08T03:14:49+00:00",
"size_bytes": 25489,
"ttl": 86400
"status": "dispatched",
"last_updated_at": "2026-07-08T03:14:49+00:00"
}
}
```
@@ -178,6 +176,7 @@ HTTP/1.1 200
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | SEO 설정에서 sitemap 생성이 비활성(`seo.sitemap_enabled=false`)인 경우 (`seo.sitemap_disabled`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
@@ -185,7 +184,11 @@ HTTP/1.1 200
**설명**
sitemap.xml 을 즉시 재생성합니다. SitemapManager 에 위임하며 큐 드라이버와 무관하게 동기(즉시) 실행되고, 완료 후 마지막 생성 시각을 갱신합니다. SEO 설정에서 sitemap 기능이 비활성인 경우 400(`seo.sitemap_disabled`), 생성 실패 시 500 을 반환합니다. 관리자가 콘텐츠 변경 후 검색엔진에 노출할 sitemap 을 스케줄 대기 없이 즉시 갱신할 때 사용합니다.
sitemap.xml 재생성을 큐에 예약합니다. 응답은 예약 접수 결과이며 생성 완료를 의미하지 않습니다 — 실제 생성은 큐 워커가 `GenerateSitemapJob` 을 처리할 때 수행되고, 완료 후 마지막 생성 시각이 갱신됩니다.
요청 스레드에서 동기 생성하지 않는 이유는 대용량(수백만 URL) 사이트에서 생성이 수 분~수십 분 걸려 관리자 요청이 메모리 초과/타임아웃으로 실패하기 때문입니다. 잡은 유니크 락(`seo-sitemap`)을 사용하므로 재생성을 연속으로 요청해도 동시에 여러 건이 실행되지 않고 한 건만 큐에 남습니다.
생성 진행 상황은 이 응답으로 알 수 없습니다. 완료 여부는 `last_updated_at` 이 갱신되었는지로 확인합니다.
### GET /api/admin/seo/stats
+37 -2
View File
@@ -12,7 +12,8 @@
5. seo-config.json: 텍스트 추출(text_props), 속성 매핑(attr_map), 허용 속성(allowed_attrs), 컴포넌트→HTML 매핑 — 모두 템플릿 선언
6. 훅 시스템: core.seo.filter_context/filter_og_data/filter_twitter_data/filter_structured_data/filter_meta/filter_view_data + 봇 감지 훅 core.seo.resolve_is_bot
7. 도메인 ownership: 모듈/플러그인이 `seoOgDefaults`/`seoTwitterDefaults`/`seoStructuredData` 메서드로 자기 도메인 OG/Twitter/JSON-LD 를 owned (이커머스 Product, 게시판 Article 등)
7. Artisan: seo:warmup, seo:clear, seo:stats, seo:generate-sitemap
8. Sitemap: 비공개 디스크에 분할 커밋(sitemapindex + sitemap-{n}.xml) 후 스트리밍 서빙 — 요청 스레드 생성 없음, 미스 시 잡 디스패치 + stale/503
9. Artisan: seo:warmup, seo:clear, seo:stats, seo:generate-sitemap
```
## 아키텍처 개요
@@ -632,6 +633,36 @@ php artisan seo:generate-sitemap # Sitemap 생성 (큐 디스패치)
php artisan seo:generate-sitemap --sync # Sitemap 동기 생성
```
## Sitemap 분할 생성과 서빙
Sitemap 은 비공개 디스크(StorageInterface, `cache` 카테고리)에 분할 파일로 커밋되고, 컨트롤러가 스트리밍으로 서빙합니다. `public/storage` 심볼릭 링크에 의존하지 않으며 자식 파일을 메모리에 적재하지 않습니다.
**디스크 레이아웃** (SSoT: `SitemapFileStore` 상수):
```text
sitemap/manifest.json 커밋 마커 + 자식 목록 메타 (이 파일 존재 = 서빙 가능)
sitemap/sitemap.xml sitemapindex (항상 비압축)
sitemap/sitemap-{n}.xml 자식 sitemap (gzip 시 .xml.gz)
sitemap/_tmp/ 생성 중 임시 디렉토리 (커밋 시 정리)
```
**라우트**: `/sitemap.xml`(인덱스) · `/sitemap-{n}.xml`, `/sitemap-{n}.xml.gz`(자식). 자식 경로는 `SitemapFileStore::childUrl()` 이 인덱스 `<loc>` 에 기록하는 값과 일치해야 합니다. gzip 여부는 manifest 가 결정하므로 두 경로를 같은 액션이 처리합니다.
**서빙 시맨틱** (요청 스레드에서 생성하지 않음):
| 상태 | 동작 |
|------|------|
| 메타 캐시 신선 + 세트 존재 | 디스크 세트 스트리밍 |
| 메타 캐시 만료 | `GenerateSitemapJob` 디스패치 후 기존 세트 서빙 (`sitemap_serve_stale_on_miss=true`) |
| 메타 캐시 만료 + stale 서빙 off | 잡 디스패치 후 503 + `Retry-After: 120` |
| 세트 전무 (신규 설치) | 잡 디스패치 후 503 + `Retry-After: 120` |
봇 요청 스레드에서 동기 생성하면 대용량(수백만 URL)에서 메모리 초과·타임아웃이 발생하므로 생성은 항상 큐가 담당합니다. 잡은 유니크 락(`seo-sitemap`)을 쓰므로 캐시 미스가 몰려도 동시에 여러 건이 실행되지 않습니다.
**분할 기준**: `sitemap_urls_per_file`(기본 50000) 또는 파일 크기 임계(`SitemapWriter::MAX_FILE_BYTES`, 45MB) 중 먼저 도달하는 쪽. 둘 다 sitemaps.org 프로토콜 제한(50,000 URL / 50MB)을 지키기 위한 것입니다.
**관리자 수동 재생성**: `POST /api/admin/seo/sitemap/regenerate` 는 큐에 예약만 하고 즉시 응답합니다(`status: dispatched`). 완료 여부는 `sitemap_last_updated_at` 갱신으로 확인합니다.
## 설정값 (코어 seo.*)
| 키 | 타입 | 기본값 | 설명 |
@@ -641,7 +672,11 @@ php artisan seo:generate-sitemap --sync # Sitemap 동기 생성
| cache_enabled | boolean | true | SEO 캐시 ON/OFF |
| cache_ttl | integer | 7200 | 캐시 TTL (초) |
| sitemap_enabled | boolean | true | sitemap.xml 생성 ON/OFF |
| sitemap_cache_ttl | integer | 86400 | Sitemap 캐시 TTL (초) |
| sitemap_cache_ttl | integer\|null | null | Sitemap 캐시 TTL 오버라이드 (초). null=고급 탭 `cache.seo_sitemap_ttl` 을 따름 |
| sitemap_urls_per_file | integer | 50000 | 자식 파일당 URL 수 (분할 기준). 1000~50000, 상한은 sitemaps.org 프로토콜 제한 |
| sitemap_gzip | boolean | false | 자식 파일 gzip 압축. 인덱스 파일은 항상 비압축 |
| sitemap_serve_stale_on_miss | boolean | true | 신선도 만료 시 기존 세트를 그대로 서빙(true) / 503 반환(false) |
| sitemap_max_urls_per_contributor | integer | 0 | 수집기당 URL 상한 (0=무제한). 초과 시 경고 로그 + truncate |
| sitemap_schedule | string | "daily" | 생성 주기 (hourly/daily/weekly) |
| sitemap_schedule_time | string | "02:00" | 생성 시각 |
| og_default_site_name | string | "" | og:site_name 기본값. 비면 `general.site_name` fallback |
+37
View File
@@ -14,12 +14,14 @@
5. 테스트 실행 중 발견한 무관 에러도 같은 세션에서 함께 처리 (stale test 수정 or 로직 수정)
6. 릴리스 전 composer test-smoke 통과 필수 (Installation 스위트)
7. 백엔드: php artisan test --filter=TestName / _bundled 확장은 _bundled에서 직접 실행
8. settings 디스크는 TestCase 가 전역 페이크 — 실제 설정 파일 오염 차단. 저장 직후 값 검증은 g7_core_settings() 말고 ConfigRepository::getCategory()
```
---
## 목차
- [실제 환경 파일 보호 (settings 디스크 격리)](#실제-환경-파일-보호-settings-디스크-격리)
- [기능 단위 시나리오 매트릭스](#기능-단위-시나리오-매트릭스)
- [도메인별 테스트 전략 매트릭스](#도메인별-테스트-전략-매트릭스)
- [Pre-release Smoke Suite](#pre-release-smoke-suite)
@@ -211,6 +213,41 @@ beforeRefreshingDatabase()는 RefreshDatabase 트레이트가 부모 클래스
---
## 실제 환경 파일 보호 (settings 디스크 격리)
`settings` 디스크의 root 는 `storage/app/settings` — 개발/운영이 실제로 사용하는 설정 파일 그 자체다. 설정 저장 경로를 타는 테스트(설정 API 호출, `ConfigRepository::saveCategory()`)는 이 파일을 그대로 덮어쓰며, `RefreshDatabase` 는 DB 만 되돌리므로 오염이 잔류한다.
`Tests\TestCase` 가 앱 부팅 직후 `Storage::fake('settings')` 로 전역 격리한다. 테스트에서 별도 조치는 필요 없다.
```text
✅ 설정 저장 테스트 = 추가 조치 불필요 (TestCase 가 이미 격리)
❌ Storage::fake('settings') 를 테스트마다 중복 호출 — 불필요
❌ 격리를 우회해 storage/app/settings 를 직접 읽고/쓰기 — 실제 환경 오염
```
### 저장 직후 값 검증
`g7_core_settings()` / `config('g7_settings.*')` 는 **부팅 시점에 적재된 값**이라 같은 프로세스에서 저장 직후에는 갱신되지 않는다. 저장 결과를 검증하려면 저장소를 직접 읽는다.
```php
// ❌ 저장했는데 부팅 시점 값이 나온다
$this->assertSame(20000, g7_core_settings('seo.sitemap_urls_per_file'));
// ✅ 저장소 실제 값 조회
$seo = app(ConfigRepositoryInterface::class)->getCategory('seo');
$this->assertSame(20000, $seo['sitemap_urls_per_file']);
```
### 배경 (회귀 사례)
격리 도입 전에는 `Storage::fake('settings')` 를 쓰는 테스트가 1개뿐이라, 설정 저장 테스트를 돌릴 때마다 개발 환경의 실제 설정이 테스트 값으로 대체됐다. 실측 피해: `general.json` 의 사이트명이 `"Test"`, 언어가 `ja` 로 바뀌어 **개발 사이트 언어가 일본어로 전환**되어 있었고, SEO/캐시 TTL 도 테스트 값으로 덮여 운영자가 지정한 값이 사라졌다. 테스트는 계속 green 이라 조용히 누적됐다.
회귀 가드: `tests/Feature/Settings/SettingsDiskIsolationTest.php` (격리 해제 시 FAIL).
`Storage::fake()` 는 **해석된 디스크 인스턴스만 교체**하고 `config('filesystems.disks.settings.root')` 는 실경로로 남는다. 격리 여부를 판정할 때는 `Storage::disk('settings')->path('')` 를 봐야 한다.
---
## 기능 단위 시나리오 매트릭스
```text
@@ -9,6 +9,8 @@
### Added
- 사이트맵 생성이 실패했을 때 표시되는 안내 문구 일본어 번역 추가 (`exceptions.seo.*`) — 사이트맵 파일을 저장하지 못하거나 압축에 실패한 경우의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
- 사이트맵 재생성 예약·준비 중·생성 실패 안내 문구 일본어 번역 추가 (`seo.sitemap_*`) — 재생성을 예약했을 때, 아직 준비 중일 때, 생성에 실패했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
- SEO 설정의 사이트맵 분할 기준·압축·수집기 상한과 SEO/사이트맵 캐시 유지 시간 항목의 검증 안내 문구 일본어 번역 추가 (`validation.settings.*`) — 잘못된 값을 입력했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
## [1.0.4] - 2026-07-16
@@ -4,12 +4,12 @@ return [
'fetch_success' => 'アクティビティログ情報を正常に取得しました。',
'fetch_failed' => 'アクティビティログ情報の取得に失敗しました。',
'delete_success' => 'アクティビティログが削除されました。',
'bulk_delete_success' => '選択したアクティビティログが削除されました。',
'bulk_delete_success' => '選択されたアクティビティログが削除されました。',
'delete_failed' => 'アクティビティログの削除に失敗しました。',
'validation' => [
'ids_required' => '削除するアクティビティログを選択してください。',
'ids_min' => '削除するアクティビティログを1つ以上選択してください。',
'id_not_found' => '選択したアクティビティログが見つかりません。',
'id_not_found' => '選択されたアクティビティログが見つかりません。',
],
'type' => [
'admin' => '管理者',
@@ -28,13 +28,13 @@ return [
'login_failed' => 'ログイン失敗',
'account_locked' => 'アカウントロック',
'request' => 'リクエスト',
'expired' => '期限切れ',
'expired' => '有効期限切れ',
'verify' => '本人認証',
'verify_failed' => '本人認証失敗',
'export' => 'エクスポート',
'import' => 'インポート',
'index' => 'リスト表示',
'show' => '詳細表示',
'index' => 'リスト閲覧',
'show' => '詳細閲覧',
'search' => '検索',
'install' => 'インストール',
'activate' => '有効化',
@@ -62,131 +62,133 @@ return [
'withdraw' => '退会',
'update_order' => '順序変更',
'refresh_layouts' => 'レイアウト更新',
'version_update' => 'バージョン更新',
'version_update' => 'バージョンアップデート',
'version_restore' => 'バージョン復元',
'reset' => '初期化',
],
'description' => [
'user_index' => 'ユーザー一覧閲覧',
'user_create' => 'ユーザー作成(ID::user_id)',
'user_show' => 'ユーザー詳細閲覧(ID::user_id)',
'user_update' => 'ユーザー編集(ID::user_id)',
'user_delete' => 'ユーザー削除(ID::user_id)',
'user_withdraw' => 'ユーザー退会(ID::user_id)',
'user_index' => 'ユーザーリスト閲覧',
'user_create' => 'ユーザー作成 (ID: :user_id)',
'user_show' => 'ユーザー詳細閲覧 (ID: :user_id)',
'user_update' => 'ユーザー編集 (ID: :user_id)',
'user_delete' => 'ユーザー削除 (ID: :user_id)',
'user_withdraw' => 'ユーザー退会 (ID: :user_id)',
'user_statistics' => 'ユーザー統計閲覧',
'user_recent' => '最近のユーザー閲覧',
'user_recent' => '最新ユーザー閲覧',
'user_search' => 'ユーザー検索',
'user_check_email' => 'メールアドレス重複確認',
'user_update_language' => 'ユーザー言語設定変更',
'user_bulk_update_status' => 'ユーザー一括ステータス変更(:count件)',
'role_index' => 'ロール一覧閲覧',
'role_active' => '有効ロール一覧閲覧',
'role_show' => 'ロール詳細閲覧(ID::role_id)',
'role_create' => 'ロール作成(ID::role_id)',
'role_update' => 'ロール編集(ID::role_id)',
'role_delete' => 'ロール削除(ID::role_id)',
'role_toggle_status' => 'ロールステータス切り替え(ID::role_id)',
'role_sync_permissions' => 'ロール権限同期(ID::role_id)',
'permission_index' => '権限一覧閲覧',
'menu_index' => 'メニュー一覧閲覧',
'menu_hierarchy' => 'メニュー階層閲覧',
'menu_active' => '有効メニュー一覧閲覧',
'menu_show' => 'メニュー詳細閲覧(ID::menu_id)',
'menu_create' => 'メニュー作成(ID::menu_id)',
'menu_update' => 'メニュー編集(ID::menu_id)',
'menu_delete' => 'メニュー削除(ID::menu_id)',
'menu_update_order' => 'メニュー順序変更',
'menu_toggle_status' => 'メニューステータス切り替え(ID::menu_id)',
'menu_sync_roles' => 'メニュー役割同期(ID::menu_id)',
'menu_get_by_extension' => '拡張別メニュー閲覧(:extension_type::extension_identifier)',
'settings_index' => '設定一覧閲覧',
'settings_save' => '設定保存',
'settings_show' => '設定閲覧',
'settings_update' => '設定編集',
'settings_system_info' => 'システム情報閲覧',
'settings_clear_cache' => 'キャッシュ初期化',
'user_bulk_update_status' => 'ユーザー一括ステータス変更 (:count件)',
'role_index' => 'ロールリスト閲覧',
'role_active' => '有効なロールリスト閲覧',
'role_show' => 'ロール詳細閲覧 (ID: :role_id)',
'role_create' => 'ロール作成 (ID: :role_id)',
'role_update' => 'ロール編集 (ID: :role_id)',
'role_delete' => 'ロール削除 (ID: :role_id)',
'role_toggle_status' => 'ロールステータス変更 (ID: :role_id)',
'role_sync_permissions' => 'ロール権限同期 (ID: :role_id)',
'permission_index' => '権限リスト閲覧',
'menu_index' => 'メニュー一覧の閲覧',
'menu_hierarchy' => 'メニュー階層の閲覧',
'menu_active' => '有効なメニュー一覧の閲覧',
'menu_show' => 'メニュー詳細の閲覧 (ID: :menu_id)',
'menu_create' => 'メニューの作成 (ID: :menu_id)',
'menu_update' => 'メニューの編集 (ID: :menu_id)',
'menu_delete' => 'メニューの削除 (ID: :menu_id)',
'menu_update_order' => 'メニューの順序変更',
'menu_toggle_status' => 'メニュー状態の切り替え (ID: :menu_id)',
'menu_sync_roles' => 'メニューロールの同期 (ID: :menu_id)',
'menu_get_by_extension' => '拡張別メニューの閲覧 (:extension_type: :extension_identifier)',
'settings_index' => '設定一覧の閲覧',
'settings_save' => '設定の保存',
'settings_show' => '設定の閲覧',
'settings_update' => '設定の編集',
'settings_system_info' => 'システム情報の閲覧',
'settings_clear_cache' => 'キャッシュの初期化',
'settings_optimize_system' => 'システム最適化',
'settings_backup_database' => 'データベースバックアップ',
'settings_get_app_key' => 'アプリキー閲覧',
'settings_app_key_regenerated' => 'アプリキー再生成',
'settings_backup' => 'バックアップ作成',
'settings_restore' => 'バックアップ復元',
'settings_backup_database' => 'データベースのバックアップ',
'settings_get_app_key' => 'アプリケーションキーの閲覧',
'settings_app_key_regenerated' => 'アプリケーションキーの再生成',
'settings_backup' => 'バックアップの作成',
'settings_restore' => 'バックアップの復元',
'settings_test_mail' => 'メール送信テスト',
'settings_test_driver_connection' => 'ドライバ接続テスト',
'settings_test_driver_connection' => 'ドライバー接続テスト',
'auth_login' => '管理者ログイン',
'auth_logout' => '管理者ログアウト',
'auth_register' => '会員登録',
'auth_forgot_password' => 'パスワード再設定申請',
'auth_reset_password' => 'パスワード再設定',
'auth_record_consents' => '利用規約同意履歴',
'schedule_index' => 'スケジュール一覧閲覧',
'schedule_show' => 'スケジュール詳細閲覧(ID::schedule_id)',
'schedule_create' => 'スケジュール作成(ID::schedule_id)',
'schedule_update' => 'スケジュール編集(ID::schedule_id)',
'schedule_delete' => 'スケジュール削除(ID::schedule_id)',
'schedule_run' => 'スケジュール手動実行(ID::schedule_id)',
'schedule_duplicate' => 'スケジュール複製',
'schedule_bulk_update_status' => 'スケジュール一括ステータス変更(:count件)',
'schedule_bulk_delete' => 'スケジュール一括削除(:count件)',
'schedule_statistics' => 'スケジュール統計閲覧',
'schedule_history' => 'スケジュール実行履歴閲覧(ID::schedule_id)',
'schedule_delete_history' => 'スケジュール履歴削除',
'dashboard_stats' => 'ダッシュボード統計閲覧',
'dashboard_resources' => 'ダッシュボードリソース閲覧',
'dashboard_activities' => 'ダッシュボードアクティビティ閲覧',
'dashboard_alerts' => 'ダッシュボード通知閲覧',
'auth_forgot_password' => 'パスワード忘却リクエスト',
'auth_reset_password' => 'パスワードリセット',
'auth_record_consents' => '利用規約同意記録',
'auth_login_failed' => 'ログイン失敗 (:email)',
'auth_account_locked' => 'アカウントロック (:attempts回失敗、:minutes分ブロック)',
'schedule_index' => 'スケジュール一覧の閲覧',
'schedule_show' => 'スケジュール詳細の閲覧 (ID: :schedule_id)',
'schedule_create' => 'スケジュールの作成 (ID: :schedule_id)',
'schedule_update' => 'スケジュールの編集 (ID: :schedule_id)',
'schedule_delete' => 'スケジュールの削除 (ID: :schedule_id)',
'schedule_run' => 'スケジュールの手動実行 (ID: :schedule_id)',
'schedule_duplicate' => 'スケジュールの複製',
'schedule_bulk_update_status' => 'スケジュール一括ステータス変更 (:count件)',
'schedule_bulk_delete' => 'スケジュール一括削除 (:count件)',
'schedule_statistics' => 'スケジュール統計の閲覧',
'schedule_history' => 'スケジュール実行履歴の閲覧 (ID: :schedule_id)',
'schedule_delete_history' => 'スケジュール履歴の削除',
'dashboard_stats' => 'ダッシュボード統計の閲覧',
'dashboard_resources' => 'ダッシュボードリソースの閲覧',
'dashboard_activities' => 'ダッシュボードアクティビティの閲覧',
'dashboard_alerts' => 'ダッシュボード通知の閲覧',
'attachment_upload' => 'ファイルアップロード',
'attachment_upload_batch' => 'ファイル一括アップロード',
'attachment_delete' => 'ファイル削除',
'attachment_delete' => 'ファイルの削除',
'attachment_bulk_delete' => 'ファイル一括削除',
'attachment_reorder' => 'ファイル順序変更',
'layout_index' => 'レイアウト一覧取得',
'layout_show' => 'レイアウト詳細取得 (:layout_path)',
'layout_update' => 'レイアウト編集 (:layout_path)',
'layout_versions_index' => 'レイアウトバージョン一覧取得 (:layout_path)',
'layout_version_show' => 'レイアウトバージョン詳細取得 (:layout_path)',
'layout_version_restore' => 'レイアウトバージョン復元 (:layout_path)',
'mail_template_update' => 'メールテンプレート編集 (:template_name)',
'mail_template_toggle_active' => 'メールテンプレート有効状態変更 (:template_name)',
'module_index' => 'モジュール一覧取得',
'module_installed' => 'インストール済みモジュール一覧取得',
'module_uninstalled' => '未インストールモジュール一覧取得',
'module_show' => 'モジュール詳細取得 (:module_name)',
'module_install' => 'モジュールインストール (:module_name)',
'module_activate' => 'モジュール有効化 (:module_name)',
'module_deactivate' => 'モジュール無効化 (:module_name)',
'module_uninstall' => 'モジュール削除 (:module_name)',
'module_uninstall_info' => 'モジュール削除情報取得 (モジュール: :module_name)',
'module_check_updates' => 'モジュールアップデート確認',
'module_update' => 'モジュールアップデート (:module_name)',
'module_refresh_layouts' => 'モジュールレイアウト更新 (:module_name)',
'module_dependent_templates' => 'モジュール依存テンプレート取得',
'attachment_reorder' => 'ファイルの順序変更',
'layout_index' => 'レイアウト一覧の閲覧',
'layout_show' => 'レイアウト詳細の閲覧 (:layout_path)',
'layout_update' => 'レイアウトの編集 (:layout_path)',
'layout_versions_index' => 'レイアウトバージョン一覧の閲覧 (:layout_path)',
'layout_version_show' => 'レイアウトバージョン詳細の閲覧 (:layout_path)',
'layout_version_restore' => 'レイアウトバージョンの復元 (:layout_path)',
'mail_template_update' => 'メールテンプレートの編集 (:template_name)',
'mail_template_toggle_active' => 'メールテンプレートの有効状態変更 (:template_name)',
'module_index' => 'モジュール一覧の閲覧',
'module_installed' => 'インストール済みモジュール一覧の閲覧',
'module_uninstalled' => '未インストールモジュール一覧の閲覧',
'module_show' => 'モジュール詳細の閲覧 (:module_name)',
'module_install' => 'モジュールのインストール (:module_name)',
'module_activate' => 'モジュールの有効化 (:module_name)',
'module_deactivate' => 'モジュールの無効化 (:module_name)',
'module_uninstall' => 'モジュールの削除 (:module_name)',
'module_uninstall_info' => 'モジュール削除情報の閲覧 (モジュール: :module_name)',
'module_check_updates' => 'モジュールアップデートの確認',
'module_update' => 'モジュールのアップデート (:module_name)',
'module_refresh_layouts' => 'モジュールレイアウトの更新 (:module_name)',
'module_dependent_templates' => 'モジュール依存テンプレートの閲覧',
'module_install_from_file' => 'ファイルからモジュールをインストール',
'module_install_from_github' => 'GitHubからモジュールをインストール',
'module_settings_save' => 'モジュール設定保存 (:module_name)',
'module_settings_reset' => 'モジュール設定初期化 (:module_name)',
'plugin_index' => 'プラグイン一覧取得',
'plugin_installed' => 'インストール済みプラグイン一覧取得',
'plugin_show' => 'プラグイン詳細取得 (:plugin_name)',
'module_settings_save' => 'モジュール設定の保存 (:module_name)',
'module_settings_reset' => 'モジュール設定の初期化 (:module_name)',
'plugin_index' => 'プラグイン一覧の閲覧',
'plugin_installed' => 'インストール済みプラグイン一覧の閲覧',
'plugin_show' => 'プラグイン詳細閲覧 (:plugin_name)',
'plugin_install' => 'プラグインインストール (:plugin_name)',
'plugin_activate' => 'プラグイン有効化 (:plugin_name)',
'plugin_deactivate' => 'プラグイン無効化 (:plugin_name)',
'plugin_uninstall' => 'プラグイン削除 (:plugin_name)',
'plugin_uninstall_info' => 'プラグイン削除情報取得 (プラグイン: :plugin_name)',
'plugin_uninstall_info' => 'プラグイン削除情報閲覧 (プラグイン: :plugin_name)',
'plugin_check_updates' => 'プラグインアップデート確認',
'plugin_update' => 'プラグインアップデート (:plugin_name)',
'plugin_refresh_layouts' => 'プラグインレイアウト更新 (:plugin_name)',
'plugin_dependent_templates' => 'プラグイン依存テンプレート取得',
'plugin_dependent_templates' => 'プラグイン依存テンプレート閲覧',
'plugin_settings_save' => 'プラグイン設定保存 (:plugin_name)',
'plugin_settings_reset' => 'プラグイン設定初期化 (:plugin_name)',
'template_index' => 'テンプレート一覧取得',
'template_show' => 'テンプレート詳細取得 (:template_name)',
'template_index' => 'テンプレート一覧の閲覧',
'template_show' => 'テンプレート詳細閲覧 (:template_name)',
'template_install' => 'テンプレートインストール (:template_name)',
'template_activate' => 'テンプレート有効化 (:template_name)',
'template_deactivate' => 'テンプレート無効化 (:template_name)',
'template_uninstall' => 'テンプレート削除 (:template_name)',
'template_check_updates' => 'テンプレートアップデート確認',
'template_check_modified_layouts' => 'テンプレート編集済みレイアウト確認 (:template_name)',
'template_check_modified_layouts' => 'テンプレート修正されたレイアウト確認 (:template_name)',
'template_update' => 'テンプレートアップデート (:template_name)',
'template_version_update' => 'テンプレートバージョンアップデート (:template_name)',
'template_install_from_file' => 'ファイルからテンプレートをインストール',
@@ -194,11 +196,9 @@ return [
'template_refresh_layouts' => 'テンプレートレイアウト更新 (:template_name)',
'core_update_check' => 'コアアップデート確認',
'core_update_update' => 'コアアップデート実行',
'activity_log_index' => 'アクティビティログ一覧取得',
'activity_log_index' => 'アクティビティログ一覧の閲覧',
'activity_log_delete' => 'アクティビティログ削除 (ID: :log_id)',
'activity_log_bulk_delete' => 'アクティビティログ一括削除 (:count件)',
'auth_login_failed' => 'ログイン失敗 (:email)',
'auth_account_locked' => 'アカウントロック (:attempts回の失敗、:minutes分間ブロック)',
],
'fields' => [
'name' => '名前',
@@ -208,7 +208,7 @@ return [
'timezone' => 'タイムゾーン',
'country' => '国',
'status' => 'ステータス',
'is_super' => 'スーパー管理者',
'is_super' => '最高管理者',
'homepage' => 'ホームページ',
'mobile' => '携帯電話',
'phone' => '電話番号',
@@ -218,10 +218,10 @@ return [
'bio' => '紹介',
'admin_memo' => '管理者メモ',
'identifier' => '識別子',
'is_active' => '有効化',
'is_active' => '有効性',
'url' => 'URL',
'icon' => 'アイコン',
'parent_id' => '親項目',
'parent_id' => '上位項目',
'order' => '順序',
'command' => 'コマンド',
'expression' => '実行周期',
@@ -19,60 +19,60 @@ return [
'success' => 'admin_layout.json 検証成功',
'failed' => 'admin_layout.json 検証失敗',
'exception' => 'admin_layout.json 検証中に例外が発生しました',
'version_error' => 'スキーマバージョン情報の取得に失敗しました',
'version_error' => 'スキーマバージョン情報取得失敗',
],
],
'loader' => [
'validation_failed' => 'モジュール :module のレイアウト検証に失敗しました: :errors',
'file_not_found' => 'レイアウトファイルが見つかりません: :path',
'json_parse_error' => 'JSON パースエラー (:file): :error',
'file_load_error' => 'ファイル読み込み中にエラーが発生しました (:path): :error',
'file_load_error' => 'ファイルロード中にエラーが発生しました (:path): :error',
'log' => [
'cache_hit' => 'キャッシュからレイアウトデータを読み込みました',
'cache_hit' => 'キャッシュからレイアウトデータをロード',
'cache_invalidated' => 'キャッシュが無効化されました',
'loaded' => 'レイアウトファイル読み込み完了',
'cached' => 'レイアウトデータをキャッシュに保存しました',
'cache_manually_invalidated' => 'キャッシュを手動で無効化しました',
'all_cache_invalidated' => 'すべてのキャッシュを無効化しました',
'file_load_error' => 'ファイル読み込みエラー',
'loaded' => 'レイアウトファイルのロード完了',
'cached' => 'レイアウトデータをキャッシュに保存',
'cache_manually_invalidated' => 'キャッシュ手動無効化',
'all_cache_invalidated' => 'すべてのキャッシュを無効化',
'file_load_error' => 'ファイルロードエラー',
],
],
'command' => [
'starting' => 'admin_layout.json ファイルの検証を開始します',
'starting' => 'admin_layout.json ファイル検証を開始します',
'validating_module' => 'モジュール :module を検証中...',
'validating_all_modules' => ':count 個のモジュール検証を開始します',
'module_layout_not_found' => 'モジュール :module の admin_layout.json ファイルが見つかりません',
'module_valid' => 'モジュール :module の検証成功',
'module_invalid' => 'モジュール :module の検証失敗: :error',
'module_valid' => 'モジュール :module 検証成功',
'module_invalid' => 'モジュール :module 検証失敗: :error',
'unexpected_error' => 'モジュール :module で予期しないエラーが発生しました: :error',
'modules_directory_not_found' => 'modules ディレクトリが見つかりません',
'no_modules_found' => '検証するモジュールがありません',
'module_summary' => 'モジュール :module の概要',
'module_summary' => 'モジュール :module サマリー',
'menu_name' => 'メニュー名',
'menu_route' => 'ルート',
'data_sources_count' => 'データソース数',
'layout_type' => 'レイアウトタイプ',
'validation_summary' => '検証結果の概要',
'validation_summary' => '検証結果サマリー',
'valid_modules_count' => '有効なモジュール',
'invalid_modules_count' => '無効なモジュール',
'missing_layouts_count' => 'レイアウトファイルなし',
'invalid_modules_details' => '無効なモジュールの詳細:',
'invalid_modules_details' => '無効なモジュール詳細:',
'missing_layouts_details' => 'レイアウトファイルがないモジュール:',
'cache_stats' => 'キャッシュ統計',
'total_cached_modules' => 'キャッシュされたモジュール数',
'cache_expiration' => 'キャッシュ有効期限',
'cached_modules_details' => 'キャッシュされたモジュールの詳細:',
'cached_modules_details' => 'キャッシュされたモジュール詳細:',
'clearing_cache' => 'キャッシュを無効化中...',
'cache_cleared' => 'キャッシュが無効化されました',
],
'success' => [
'loaded' => 'admin_layout.json の読み込みに成功しました',
'loaded' => 'admin_layout.json を正常に読み込みました',
'cached' => 'レイアウトデータがキャッシュに保存されました',
],
'error' => [
'module_not_found' => '要求されたモジュールが見つかりません',
'json_parse_error' => 'JSON パース中にエラーが発生しました',
'file_not_readable' => 'ファイルを読めません',
'file_not_readable' => 'ファイルを読み込めません',
'json_parse_failed' => 'JSON パースに失敗しました',
'validation_failed' => 'レイアウト検証に失敗しました',
'server_error' => 'サーバー内部エラーが発生しました',
@@ -80,14 +80,14 @@ return [
'log' => [
'cache_hit' => 'キャッシュからレイアウトデータを取得しました',
'cache_updated' => 'キャッシュが更新されました',
'layout_loaded' => 'レイアウトが正常に読み込まれました',
'layout_loaded' => 'レイアウトが正常にロードされました',
'validation_completed' => 'レイアウト検証が完了しました',
'layout_load_error' => 'レイアウト読み込み中にエラーが発生しました',
'layout_load_error' => 'レイアウトロード中にエラーが発生しました',
'validation_error' => 'レイアウト検証中にエラーが発生しました',
'service_cache_invalidated' => 'サービスでキャッシュを無効化しました',
'service_all_cache_invalidated' => 'サービスですべてのキャッシュを無効化しました',
'service_cache_invalidated' => 'サービスからキャッシュを無効化しました',
'service_all_cache_invalidated' => 'サービスからすべてのキャッシュを無効化しました',
'cache_invalidated' => 'キャッシュが無効化されました',
'all_cache_invalidated' => 'すべてのキャッシュが無効化されました',
'file_read_error' => 'ファイル読み取り中にエラーが発生しました',
'file_read_error' => 'ファイル読み込み中にエラーが発生しました',
],
];
@@ -13,18 +13,18 @@ return [
],
'default_policy' => [
'public' => '公開 (すべての認証ユーザー)',
'owner_only' => 'アップローダー/管理者のみ',
'owner_only' => 'アップロード者/管理者のみ',
'read_only' => '読み取り専用',
],
'validation' => [
'file_required' => 'ファイルを選択してください。',
'file_invalid' => '有効なファイルではありません。',
'file_max' => 'ファイルサイズは :maxMB を超えることはできません。',
'file_max' => 'ファイルサイズは :maxMB を超えることができません。',
'files_required' => 'ファイルを選択してください。',
'files_array' => 'ファイル形式が正しくありません。',
'files_min' => '最低1つ以上のファイルを選択してください。',
'type_invalid' => '添付対象タイプが正しくありません。',
'id_invalid' => '添付対象IDが正しくありません。',
'type_invalid' => '添付対象のタイプが正しくありません。',
'id_invalid' => '添付対象のIDが正しくありません。',
'role_ids_required' => 'ロールを選択してください。',
'role_ids_invalid' => 'ロール形式が正しくありません。',
'role_id_integer' => 'ロールIDは整数である必要があります。',
@@ -41,17 +41,17 @@ return [
],
'upload_success' => 'ファイルがアップロードされました。',
'upload_batch_success' => 'ファイルが一括アップロードされました。',
'upload_failed' => 'ファイルアップロードに失敗しました。',
'upload_failed' => 'ファイルのアップロードに失敗しました。',
'delete_success' => 'ファイルが削除されました。',
'delete_failed' => 'ファイル削除に失敗しました。',
'reorder_success' => 'ファイル順序が変更されました。',
'reorder_failed' => 'ファイル順序の変更に失敗しました。',
'delete_failed' => 'ファイルの削除に失敗しました。',
'reorder_success' => 'ファイルの順序が変更されました。',
'reorder_failed' => 'ファイルの順序変更に失敗しました。',
'sync_roles_success' => 'ロールが同期されました。',
'sync_roles_failed' => 'ロール同期に失敗しました。',
'sync_permissions_success' => '権限が同期されました。',
'sync_permissions_failed' => '権限同期に失敗しました。',
'not_found' => '添付ファイルが見つかりません。',
'access_denied' => 'この添付ファイルへのアクセス権限がありません。',
'update_denied' => 'この添付ファイルの編集権限がありません。',
'delete_denied' => 'この添付ファイルの削除権限がありません。',
'update_denied' => 'この添付ファイルへの編集権限がありません。',
'delete_denied' => 'この添付ファイルへの削除権限がありません。',
];
@@ -1,16 +1,16 @@
<?php
return [
'login_success' => 'ログインに成功しました。',
'login_success' => 'ログインが成功しました。',
'login_failed' => '認証に失敗しました。',
'register_success' => '会員登録に成功しました。',
'register_success' => '会員登録が成功しました。',
'register_failed' => '会員登録に失敗しました。',
'logout_success' => 'ログアウトに成功しました。',
'logout_success' => 'ログアウトが成功しました。',
'logout_all_devices_success' => 'すべてのデバイスからログアウトされました。',
'password_reset_success' => 'パスワードが正常にリセットされました。',
'password_reset_failed' => 'パスワードのリセットに失敗しました。',
'invalid_credentials' => '無効な認証情報です。',
'admin_login_success' => '管理者ログインに成功しました。',
'admin_login_success' => '管理者ログインが成功しました。',
'admin_required' => '管理者権限が必要です。',
'unauthenticated' => '認証が必要です。',
'permission_denied' => '該当する権限がありません。',
@@ -23,17 +23,17 @@ return [
'scope_denied' => 'このリソースへのアクセス権限がありません。',
'scope_type_all' => 'すべて',
'scope_type_self' => '本人のみ',
'scope_type_role' => '所有者ロール',
'scope_type_role' => '所有ロール',
'invalid_token' => '無効な認証トークンです。',
'token_expired' => '認証トークンが期限切れです。もう一度ログインしてください。',
'token_expired' => '認証トークンが有効期限切れです。再度ログインしてください。',
'user_not_found' => 'ユーザーが見つかりません。',
'account_inactive' => '無効化されたアカウントです。管理者にお問い合わせください。',
'account_blocked' => 'ブロックされたアカウントです。管理者にお問い合わせください。',
'account_withdrawn' => '退会済みのアカウントです。',
'account_pending_verification' => '本人確認が完了していないアカウントです。メール認証を完了してください。',
'reset_token_invalid' => '無効なパスワードリセットトークンです。',
'reset_token_expired' => 'パスワードリセットトークンが期限切れです。再度リクエストしてください。',
'password_reset_email_sent' => 'パスワードリセットメールが送信されました。',
'email_send_failed' => 'メール送信に失敗しました。しばらく後にもう一度お試しください。',
'account_withdrawn' => '退会したアカウントです。',
'account_pending_verification' => '本人認証が完了していないアカウントです。メール認証を完了してください。',
'account_locked' => 'ログイン試行回数の超過によりアカウントがロックされました。:minutes分後に再度お試しください。',
'reset_token_invalid' => '無効なパスワードリセットトークンです。',
'reset_token_expired' => 'パスワードリセットトークンが有効期限切れです。再度リクエストしてください。',
'password_reset_email_sent' => 'パスワードリセットメールが送信されました。',
'email_send_failed' => 'メール送信に失敗しました。しばらく後に再度お試しください。',
];
@@ -17,17 +17,17 @@ return [
'unknown' => '不明',
'errors' => [
'github_url_empty' => 'GitHub URLが空です。',
'github_url_invalid' => '無効なGitHub URL形式です。',
'github_url_invalid' => '無効なGitHub URLフォーマットです。',
'github_api_failed' => 'GitHub APIの呼び出しに失敗しました。',
'github_download_failed' => 'GitHubからのダウンロードに失敗しました。',
'github_archive_download_failed' => 'アーカイブのダウンロードに失敗しました。(:url)',
'zip_file_not_found' => 'ZIPファイルが見つかりません。',
'zip_open_failed' => 'ZIPファイルを開くことができません。',
],
'validation_error' => '入力値バリデーションエラー',
'validation_error' => '入力値のバリデーションエラー',
'changelog_validation' => [
'source_in' => 'ソースはactive、bundled、githubのいずれかである必要があります。',
'version_format' => ':attribute形式が正しくありません。(例: 1.0.0、1.0.0-beta.1)',
'version_format' => ':attributeのフォーマットが正しくありません。(例:1.0.0、1.0.0-beta.1)',
'to_version_required' => '開始バージョンが指定されている場合、終了バージョンも必須です。',
],
];
@@ -1,14 +1,16 @@
<?php
return [
'stats_loaded' => 'ダッシュボード統計を正常に閲覧しました。',
'stats_failed' => 'ダッシュボード統計の閲覧に失敗しました。',
'resources_loaded' => 'システムリソース情報を正常に閲覧しました。',
'resources_failed' => 'システムリソース情報の閲覧に失敗しました。',
'activities_loaded' => '最近のアクティビティログを正常に閲覧しました。',
'activities_failed' => '最近のアクティビティログの閲覧に失敗しました。',
'alerts_loaded' => 'システム通知を正常に閲覧しました。',
'alerts_failed' => 'システム通知の閲覧に失敗しました。',
'stats_loaded' => 'ダッシュボード統計の照会に成功しました。',
'stats_failed' => 'ダッシュボード統計の照会に失敗しました。',
'resources_loaded' => 'システムリソース情報の照会に成功しました。',
'resources_failed' => 'システムリソース情報の照会に失敗しました。',
'activities_loaded' => '最近のアクティビティの照会に成功しました。',
'activities_failed' => '最近のアクティビティの照会に失敗しました。',
'alerts_loaded' => 'システム通知の照会に成功しました。',
'alerts_failed' => 'システム通知の照会に失敗しました。',
'recent_notifications_loaded' => '最近の通知の照会に成功しました。',
'recent_notifications_failed' => '最近の通知の照会に失敗しました。',
'stats' => [
'status_normal' => '正常',
'status_warning' => '警告',
@@ -16,6 +18,4 @@ return [
'activities' => [
'user_registered' => ':name さんが会員登録しました。',
],
'recent_notifications_loaded' => '最近の通知を正常に閲覧しました。',
'recent_notifications_failed' => '最近の通知の閲覧に失敗しました。',
];
@@ -11,16 +11,16 @@ return [
],
'404' => [
'title' => 'ページが見つかりません',
'message' => 'リクエストされたページが存在しないか、移動されています。',
'message' => '要求されたページが存在しないか、移動した可能性があります。',
],
'500' => [
'title' => 'サーバーエラーが発生しました',
'message' => 'リクエストを処理する際に問題が発生しました。しばらく後に再度試してください。',
'message' => 'リクエストを処理する際に問題が発生しました。しばらく後に再度お試しください。',
],
'503' => [
'title' => 'サービス一時中断',
'message' => '現在、このサービスは利用できません。しばらく後に再度試してください。',
'unmet_dependencies' => '未解決の依存関係リスト',
'title' => 'サービス一時停止',
'message' => '現在サービスをご利用いただけません。しばらく後に再度お試しください。',
'unmet_dependencies' => '未充足の依存性リスト',
'template' => 'テンプレート',
'modules' => 'モジュール',
'plugins' => 'プラグイン',
@@ -29,7 +29,7 @@ return [
'back_home' => 'ホームに戻る',
'bootstrap' => [
'title' => '画面を読み込めませんでした',
'message' => 'ネットワーク接続が不安定な可能性があります。更新後にもう一度お試しください。',
'message' => 'ネットワーク接続が不安定な可能性があります。ページを更新してからもう一度お試しください。',
'reload' => '更新',
],
];
@@ -3,22 +3,23 @@
return [
'cannot_delete_super_admin' => 'スーパー管理者は削除できません。',
'circular_reference' => 'レイアウト循環参照を検出しました: :trace',
'max_depth_exceeded' => 'レイアウトのネスト深度が最大許容深度(:max)を超えています。',
'max_depth_exceeded' => 'レイアウトネストの深さが最大許容深度(:max)を超過しました。',
'template_file_copy_failed' => 'テンプレートファイルのコピーに失敗しました: :source → :destination',
'template_build_directory_creation_failed' => 'テンプレートビルドディレクトリの作成に失敗しました: :path',
'template_build_directory_creation_failed' => 'テンプレートビルドディレクトリの生成に失敗しました: :path',
'template_dist_directory_not_found' => 'テンプレートdistディレクトリが見つかりません: :path',
'template_not_found' => 'テンプレートが見つかりません: :identifier',
'template_not_active' => 'テンプレートが有効化されていません: :identifier (ステータス: :status)',
'concurrent_modification' => '別のユーザーが先に保存しました。 (リソース: :resource、現在のバージョン: :current、送信されたバージョン: :expected)',
'layout' => [
'duplicate_data_source_id' => 'data_sources IDが重複しています: :id',
'duplicate_data_source_id_in_file' => 'レイアウトファイル内のdata_sources IDが重複しています: :ids (ファイル: :file)',
'duplicate_data_source_id_extends' => 'extends継承関係でdata_sources IDが重複しています: :ids (子: :child, 親: :parent)',
'duplicate_data_source_id_extends' => 'extends継承関係のdata_sources IDが重複しています: :ids (子: :child、親: :parent)',
'not_found' => 'レイアウトが見つかりません: :name',
'parent_not_found' => '親レイアウトが見つかりません: :parent (リクエストレイアウト: :child)',
'parent_not_found' => '親レイアウトが見つかりません: :parent (要求レイアウト: :child)',
'include_file_not_found' => 'includeファイルが見つかりません: :path (解決されたパス: :resolved)',
'invalid_include_json' => 'includeファイルのJSON形式が正しくありません: :path (エラー: :error)',
'circular_include' => 'include循環参照が検出されました: :trace',
'max_include_depth_exceeded' => 'includeの最大深度を超えています (最大: :max段階)',
'max_include_depth_exceeded' => 'include最大深度を超過しました (最大: :max段階)',
'include_outside_directory' => 'includeパスが許可されたディレクトリ外です: :path (許可: :allowed_dir)',
],
'layout_version' => [
@@ -26,46 +27,45 @@ return [
'save_failed_unexpected' => 'レイアウトバージョン保存中に予期しないエラーが発生しました。',
],
'settings' => [
'backup_creation_failed' => '設定バックアップファイルの作成に失敗しました。',
'backup_creation_failed' => '設定バックアップファイルの生成に失敗しました。',
'restore_failed' => '設定の復元に失敗しました。',
'category_not_found' => '設定カテゴリが見つかりません: :category',
'save_failed' => '設定の保存に失敗しました: :category',
'json_parse_failed' => 'JSON ファイルのパースに失敗しました: :path (:error)',
'add_category_module_only' => 'addCategory はモジュールでのみ使用できます。',
'json_parse_failed' => 'JSONファイルのパースに失敗しました: :path (:error)',
'add_category_module_only' => 'addCategoryはモジュールからのみ使用できます。',
],
'core_update' => [
'handoff' => 'アップグレードハンドオフ: :after_version まで完了 — :reason (再実行: :resume_command)',
],
'vendor' => [
'composer_not_available' => 'Composerを実行できない環境です。バンドルモードを使用してください。',
'composer_not_available_for_build' => 'vendor-bundleビルドはComposer実行が必要です。開発環境でComposerをインストール·設定した後、もう一度お試しください。',
'composer_not_available_for_build' => 'vendor-bundleビルドはComposer実行が必要です。開発環境でComposerをインストール/設定した後、再度実行してください。',
'bundle_build_composer_failed' => 'vendor-bundleビルド中にcomposer installの実行に失敗しました (exit :exit): :message',
'bundle_build_promote_failed' => 'vendor-bundleビルド結果を最終パスに反映できませんでした (:from → :to)',
'composer_execution_failed' => 'Composerの実行に失敗しました: :message',
'bundle_zip_missing' => 'vendor-bundle.zipファイルが見つかりません: :path',
'bundle_manifest_missing' => 'vendor-bundle.jsonファイルが見つかりません: :path',
'bundle_manifest_invalid' => 'vendor-bundle.jsonファイルの読み込みに失敗しました: :error',
'bundle_integrity_failed' => 'バンドルファイルの整合性検証に失敗しました: :details',
'bundle_manifest_invalid' => 'vendor-bundle.jsonファイルを読み込めません: :error',
'bundle_integrity_failed' => 'バンドルファイルの完全性検証に失敗しました: :details',
'bundle_schema_unsupported' => 'サポートされていないvendor-bundle.jsonスキーマバージョンです: :version',
'zip_archive_not_available' => 'PHP ZipArchive拡張がインストールされていません。',
'zip_hash_mismatch' => 'vendor-bundle.zipのハッシュが一致しません (期待値: :expected, 実際: :actual)。',
'zip_hash_mismatch' => 'vendor-bundle.zipのハッシュが一致しません (期待値: :expected、実際: :actual)。',
'composer_json_sha_mismatch' => 'composer.jsonがバンドルビルド時点以降に変更されました。vendor-bundle:buildで再ビルドしてください。',
'composer_lock_sha_mismatch' => 'composer.lockがバンドルビルド時点以降に変更されました。vendor-bundle:buildで再ビルドしてください。',
'bundle_contains_unsafe_path' => 'バンドルzipに安全でないファイルパスが含まれています: :path',
'extraction_failed' => 'バンドルzipの抽出に失敗しました: :message',
'no_vendor_strategy_available' => 'バンドルのインストール戦略を決定できません。Composerとバンドルzipの両方が利用不可です。',
'no_vendor_strategy_available' => 'Vendorインストール戦略を決定できません。Composerとバンドルzipのどちらも使用できません。',
'source_dir_not_found' => 'インストールソースディレクトリが見つかりません: :path',
'target_not_writable' => '対象ディレクトリに書き込み権限がありません: :path',
'target_not_writable_owner_hint' => '実行ユーザー(:current_user)とディレクトリ所有者(:owner)が異なります。\'chown -R :current_user_name :path\'を実行後に再試行するか、所有者(:owner_name)でSSHログイン後に実行してください。',
'target_not_writable' => '対象ディレクトリへの書き込み権限がありません: :path',
'target_not_writable_owner_hint' => '実行ユーザー(:current_user)とディレクトリ所有者(:owner)が異なります。\'chown -R :current_user_name :path\'を実行後に再度試すか、所有者(:owner_name)でSSHログイン後に実行してください。',
'composer_json_not_found' => 'composer.jsonファイルが見つかりません: :path',
'no_composer_lock' => 'composer.lockファイルが必要です。まずcomposer installを実行してください: :path',
'vendor_dir_not_found' => 'vendor/ディレクトリが存在しません。まずcomposer installを実行してください: :path',
'no_composer_lock' => 'composer.lockファイルが必要です。最初にcomposer installを実行してください: :path',
'vendor_dir_not_found' => 'vendor/ディレクトリが存在しません。最初にcomposer installを実行してください: :path',
'composer_binary_not_found' => 'Composerバイナリが見つかりません。COMPOSER_BINARY環境変数を設定するか、composerをPATHに追加してください。',
'bundle_build_promote_failed' => 'vendor-bundle ビルド結果を最終パスに反映できませんでした (:from → :to)',
],
'concurrent_modification' => '別のユーザーが先に保存しました。(リソース: :resource、現在のバージョン: :current、送信されたバージョン: :expected)',
'seo' => [
'sitemap_write_failed' => 'Sitemapファイル記録に失敗しました: :path',
'sitemap_gzip_failed' => 'Sitemap子ファイル圧縮に失敗しました: :file',
'sitemap_write_failed' => 'Sitemapファイルの記録に失敗しました: :path',
'sitemap_gzip_failed' => 'Sitemap子ファイルの圧縮に失敗しました: :file',
'sitemap_temp_file_unreadable' => 'Sitemap一時ファイルを読み込めません: :file',
'sitemap_stat_group_column_unsupported' => 'サポートされていないグループ基準列です: :column',
],
@@ -7,51 +7,51 @@ return [
'template' => 'テンプレート',
],
'errors' => [
'core_version_mismatch' => ':extension (:type)はグヌボード7 コアバージョン :required 以上が必要です。(現在: :installed)',
'core_version_mismatch' => ':extension (:type)はグヌボード7コアバージョン :required 以上が必要です。(現在: :installed)',
'version_check_failed' => 'バージョン検証に失敗しました。',
'operation_in_progress' => '":name"に対して実行中の処理(:status)があるため、リクエストを処理できません。',
'operation_in_progress' => '":name"に対して進行中の処理(:status)があるため、リクエストを処理できません。',
'zip_missing_manifest' => 'ZIP内から :file マニフェストが見つかりません: :zip',
'zip_invalid_manifest' => 'ZIP内の :file マニフェストをJSONとして解析できません。',
'zip_identifier_mismatch' => 'ZIPマニフェストの識別子がターゲット拡張と一致しません。(期待値: :expected、実際: :actual)',
'zip_missing_version' => 'ZIP内の :file マニフェストに version フィールドがありません。',
'zip_identifier_mismatch' => 'ZIPマニフェストの識別子が対象拡張と一致しません。(期待値: :expected、実際: :actual)',
'zip_missing_version' => 'ZIP内の :file マニフェストにversionフィールドがありません。',
'not_found' => '拡張(:identifier)が見つかりません。',
'cascade_dependency_failed' => '付属インストール対象の :type (:identifier)のインストールに失敗しました: :message',
'cascade_dependency_failed' => '同梱インストール対象の :type (:identifier)のインストールに失敗しました: :message',
'invalid_type' => '無効な拡張タイプです。',
'not_auto_deactivated' => 'この拡張はコアバージョンの互換性の問題により自動無効化された状態ではありません。',
'hidden_extension' => '内部用(hidden)拡張はユーザーに公開されません。',
'not_auto_deactivated' => 'この拡張はコアバージョン互換性の問題により自動無効化された状態ではありません。',
'hidden_extension' => '内部用(非表示)拡張はユーザーに非公開です。',
],
'warnings' => [
'auto_deactivated' => ':type ":identifier"はコアバージョン互換性の問題により自動的に無効化されました。',
'auto_deactivated' => ':type ":identifier"がコアバージョン互換性の問題により自動無効化されました。',
],
'alerts' => [
'incompatible_deactivated' => ':type ":name"自動無効化',
'incompatible_message' => '必須バージョン: :required、現在インストール済み: :installed',
'recovered_title' => ':type「:name」再び互換性があります',
'incompatible_message' => '必要バージョン: :required、現在インストール済み: :installed',
'recovered_title' => ':type ":name"互換性を再取得',
'recovered_body' => 'コアアップグレード後に互換性があります(以前の要件: :previously_required)。再度有効化できます。',
'recovered_success' => '拡張が再び有効化されました。',
'recovered_success' => '拡張が再度有効化されました。',
'dismissed' => '通知を閉じました。',
'auto_deactivated_listed' => '自動無効化された拡張のリストです。',
'auto_deactivated_listed' => '自動無効化された拡張の一覧です。',
'recover_action' => '再度有効化',
'dismiss_action' => '通知を閉じる',
],
'commands' => [
'clear_cache_success' => '拡張バージョン検証キャッシュが削除されました。',
],
'badges' => [
'incompatible' => 'コアアップグレードが必要です',
'incompatible_tooltip' => 'コア:required以上が必要です(現在: :installed)',
'incompatible_sr' => ':nameはコア:required以上が必要ですが、現在:installedがインストールされているため更新できません。',
'incompatible' => 'コアアップグレード必要',
'incompatible_tooltip' => 'コア :required 以上が必要です(現在: :installed)',
'incompatible_sr' => ':name はコア :required 以上が必要ですが、現在 :installed がインストールされているため、アップデートできません。',
],
'banner' => [
'title' => 'コア互換性の問題により自動無効化された拡張があります',
'item_required' => '必須バージョン: :required',
'item_required' => '必要バージョン: :required',
'guide_link' => 'コアアップグレードガイド',
'dismiss' => '閉じる',
],
'update_modal' => [
'compat_warning_title' => 'コアバージョン互換性警告',
'compat_warning_message' => 'この:typeはコア:required以上が必要です。(現在: :installed)',
'compat_warning_message' => 'この :type はコア :required 以上が必要です。(現在: :installed)',
'compat_guide_link' => 'コアアップグレードガイドを表示',
'force_label' => '警告を無視して強制更新します(推奨されません)',
'force_label' => '警告を無視して強制アップデート(非推奨)',
],
'commands' => [
'clear_cache_success' => '拡張バージョン検証キャッシュが削除されました。',
],
];
@@ -1,7 +1,7 @@
<?php
return [
'cache_generated' => '静的フック マッピング キャッシュを作成しました。(コア :core / モジュール :modules / プラグイン :plugins) → :path',
'cache_generated' => '静的フック マッピング キャッシュを生成しました。(コア :core / モジュール :modules / プラグイン :plugins) → :path',
'cache_cleared' => '静的フック マッピング キャッシュを削除しました。',
'cache_absent' => '削除する静的フック マッピング キャッシュがありません。',
];
@@ -6,9 +6,9 @@ return [
'label' => 'メール',
'settings' => [
'code_length' => '認証コード長',
'code_length_help' => '送信される数字コードのけた数(デフォルト6、最小4、最大10)。',
'from_address' => '送信者アドレス',
'from_address_help' => '空の場合、システムデフォルトの送信者が使用されます。',
'code_length_help' => '送信される数字コードの桁数 (デフォルト 6、最小 4、最大 10)。',
'from_address' => '差出人アドレス',
'from_address_help' => '空の場合、システムのデフォルト差出人を使用します。',
],
],
],
@@ -18,48 +18,48 @@ return [
'wrong_provider' => 'この認証リクエストは別のプロバイダーで処理する必要があります。',
'invalid_state' => '既に処理された認証リクエストです。',
'expired' => '認証時間が期限切れになりました。もう一度お試しください。',
'max_attempts' => '試行回数を超えました。再度リクエストしてください。',
'max_attempts' => '試行回数を超過しました。もう一度リクエストしてください。',
'invalid_code' => '認証コードが正しくありません。',
'invalid_verification_token' => '無効な本人確認トークンです。',
'missing_target' => '認証対象(メール·電話番号)が必要です。',
'target_mismatch' => '認証した対象と要求した対象が一致しません。',
'invalid_verification_token' => '無効な本人認証トークンです。',
'missing_target' => '認証対象 (メール·電話番号) が必要です。',
'target_mismatch' => '認証された対象とリクエストされた対象が一致しません。',
'purpose_not_supported' => '選択されたプロバイダーはこの目的をサポートしていません。',
'provider_unavailable' => '本人確認プロバイダーを使用できません。',
'generic' => '本人確認に失敗しました。',
'missing_scope_or_target' => 'ポリシー照会にはscopeとtargetの両方が必要です。',
'provider_unavailable' => '本人認証プロバイダーを使用できません。',
'generic' => '本人認証に失敗しました。',
'missing_scope_or_target' => 'ポリシー照会には scope と target の両方が必要です。',
'admin_policy_has_no_default' => '管理者が直接作成したポリシーには宣言デフォルト値がありません。',
'reset_field_failed' => '宣言デフォルト値の復元に失敗しました。フィールドが有効であることを確認してください。',
],
'messages' => [
'challenge_requested' => '本人確認コードを送信しました。',
'challenge_requested' => '本人認証コードを送信しました。',
'challenge_verified' => '本人確認が完了しました。',
'challenge_cancelled' => '本人確認リクエストがキャンセルされました。',
'challenge_cancelled' => '本人認証リクエストがキャンセルされました。',
],
'logs' => [
'activity' => [
'requested' => ':emailに本人確認コードを送信しました。',
'requested' => ':email に本人認証コードを送信しました。',
'verified' => '本人確認が完了しました。',
'failed' => '本人確認に失敗しました。',
'expired' => '本人確認時間が期限切れになりました。',
'cancelled' => '本人確認リクエストをキャンセルしました。',
'failed' => '本人認証に失敗しました。',
'expired' => '本人認証時間が期限切れになりました。',
'cancelled' => '本人認証リクエストをキャンセルしました。',
],
],
'purposes' => [
'signup' => [
'label' => '会員登録認証',
'description' => '新規登録者のメール/電話番号所有確認。',
'description' => '新規ユーザーのメール/電話番号所有確認。',
],
'password_reset' => [
'label' => 'パスワード再設定',
'description' => 'パスワードを忘れたユーザーが本人確認後に再設定します。',
'description' => 'パスワードを忘れたユーザーが本人確認後に再設定。',
],
'self_update' => [
'label' => '自己情報変更',
'description' => 'ログインユーザーがメール/電話などの本人情報を変更する場合。',
'label' => '自分の情報を変更',
'description' => 'ログインユーザーがメール/電話など本人情報を変更する場合。',
],
'sensitive_action' => [
'label' => '機密作業',
'description' => 'アカウント削除·管理者作業など再認証が必要な時点。',
'label' => '機密操作',
'description' => 'アカウント削除·管理者操作など再認証が必要な時点。',
],
],
'channels' => [
@@ -70,7 +70,7 @@ return [
'hook' => 'フック',
'policy' => 'ポリシー',
'middleware' => 'ミドルウェア',
'api' => 'API直接呼び出し',
'api' => 'API 直接呼び出し',
'custom' => 'カスタム',
'system' => 'システム',
],
@@ -81,7 +81,7 @@ return [
'custom' => 'カスタム',
],
'fail_mode' => [
'block' => 'ブロック(HTTP 428)',
'block' => 'ブロック (HTTP 428)',
'log_only' => 'ログのみ記録',
],
'applies_to' => [
@@ -1,27 +1,27 @@
<?php
return [
'definition_list_success' => 'IDVメッセージ定義リストを取得しました。',
'definition_list_failed' => 'IDVメッセージ定義リスト取得に失敗しました。',
'definition_show_success' => 'IDVメッセージ定義詳細を取得しました。',
'definition_show_failed' => 'IDVメッセージ定義詳細取得に失敗しました。',
'definition_created' => 'IDVメッセージ定義が作成されました。',
'definition_create_failed' => 'IDVメッセージ定義作成に失敗しました。',
'definition_updated' => 'IDVメッセージ定義が編集されました。',
'definition_update_failed' => 'IDVメッセージ定義編集に失敗しました。',
'definition_toggled' => 'IDVメッセージ定義の有効状態が変更されました。',
'definition_toggle_failed' => 'IDVメッセージ定義有効状態変更に失敗しました。',
'definition_reset' => 'IDVメッセージ定義がデフォルト値に復元されました。',
'definition_reset_failed' => 'IDVメッセージ定義デフォルト値復元に失敗しました。',
'definition_deleted' => 'IDVメッセージ定義が削除されました。',
'definition_delete_failed' => 'IDVメッセージ定義削除に失敗しました。',
'definition_delete_forbidden' => 'シード済みのデフォルトメッセージ定義は削除できません。',
'template_updated' => 'IDVメッセージテンプレートが編集されました。',
'template_update_failed' => 'IDVメッセージテンプレート編集に失敗しました。',
'template_toggled' => 'IDVメッセージテンプレートの有効状態が変更されました。',
'template_toggle_failed' => 'IDVメッセージテンプレート有効状態変更に失敗しました。',
'template_reset' => 'IDVメッセージテンプレートがデフォルト値に復元されました。',
'template_reset_failed' => 'IDVメッセージテンプレートデフォルト値復元に失敗しました。',
'template_preview_success' => 'IDVメッセージテンプレートプレビューを生成しました。',
'template_preview_failed' => 'IDVメッセージテンプレートプレビュー生成に失敗しました。',
'definition_list_success' => 'IDV メッセージ定義リストを取得しました。',
'definition_list_failed' => 'IDV メッセージ定義リスト取得に失敗しました。',
'definition_show_success' => 'IDV メッセージ定義の詳細を取得しました。',
'definition_show_failed' => 'IDV メッセージ定義の詳細取得に失敗しました。',
'definition_created' => 'IDV メッセージ定義が作成されました。',
'definition_create_failed' => 'IDV メッセージ定義の作成に失敗しました。',
'definition_updated' => 'IDV メッセージ定義が編集されました。',
'definition_update_failed' => 'IDV メッセージ定義の編集に失敗しました。',
'definition_toggled' => 'IDV メッセージ定義の有効状態が変更されました。',
'definition_toggle_failed' => 'IDV メッセージ定義の有効状態変更に失敗しました。',
'definition_reset' => 'IDV メッセージ定義がデフォルト値に復元されました。',
'definition_reset_failed' => 'IDV メッセージ定義のデフォルト値復元に失敗しました。',
'definition_deleted' => 'IDV メッセージ定義が削除されました。',
'definition_delete_failed' => 'IDV メッセージ定義の削除に失敗しました。',
'definition_delete_forbidden' => 'シードされたデフォルトメッセージ定義は削除できません。',
'template_updated' => 'IDV メッセージテンプレートが編集されました。',
'template_update_failed' => 'IDV メッセージテンプレートの編集に失敗しました。',
'template_toggled' => 'IDV メッセージテンプレートの有効状態が変更されました。',
'template_toggle_failed' => 'IDV メッセージテンプレートの有効状態変更に失敗しました。',
'template_reset' => 'IDV メッセージテンプレートがデフォルト値に復元されました。',
'template_reset_failed' => 'IDV メッセージテンプレートのデフォルト値復元に失敗しました。',
'template_preview_success' => 'IDV メッセージテンプレートのプレビューが作成されました。',
'template_preview_failed' => 'IDV メッセージテンプレートのプレビュー作成に失敗しました。',
];
@@ -2,61 +2,61 @@
return [
'fetch_success' => '言語パックリストを取得しました。',
'fetch_failed' => '言語パックリストを読み込めませんでした。',
'fetch_failed' => '言語パックリストを読み込めませんでした: :error',
'not_found' => '言語パックが見つかりません。',
'install_success' => '言語パックをインストールしました。',
'install_failed' => '言語パックのインストールに失敗しました。',
'install_failed' => '言語パックのインストールに失敗しました: :error',
'activate_success' => '言語パックを有効化しました。',
'activate_failed' => '言語パックの有効化に失敗しました。',
'activate_failed' => '言語パックの有効化に失敗しました: :error',
'deactivate_success' => '言語パックを無効化しました。',
'deactivate_failed' => '言語パックの無効化に失敗しました。',
'deactivate_failed' => '言語パックの無効化に失敗しました: :error',
'uninstall_success' => '言語パックを削除しました。',
'uninstall_failed' => '言語パックの削除に失敗しました。',
'uninstall_failed' => '言語パックの削除に失敗しました: :error',
'manifest_invalid' => 'language-pack.json の検証に失敗しました。',
'check_updates_success' => 'アップデート確認を完了しました。',
'check_updates_failed' => 'アップデート確認に失敗しました。',
'check_updates_success' => 'アップデート確認が完了しました。',
'check_updates_failed' => 'アップデート確認に失敗しました: :error',
'update_success' => '言語パックをアップデートしました。',
'update_failed' => '言語パックのアップデートに失敗しました。',
'update_failed' => '言語パックのアップデートに失敗しました: :error',
'refresh_cache_success' => '言語パックキャッシュを更新しました。',
'refresh_cache_failed' => '言語パックキャッシュの更新に失敗しました。',
'preview_success' => 'manifestプレビューを完了しました。',
'preview_failed' => 'manifestプレビューに失敗しました。',
'refresh_cache_failed' => '言語パックキャッシュの更新に失敗しました: :error',
'preview_success' => 'manifest プレビューが完了しました。',
'preview_failed' => 'manifest プレビューに失敗しました: :error',
'errors' => [
'manifest_not_found' => 'ZIP内でlanguage-pack.jsonファイルが見つかりません。',
'manifest_invalid_json' => 'language-pack.json のJSON形式が正しくありません。',
'php_outside_backend' => 'backend/ディレクトリ外にPHPファイルが含まれています (:file)。',
'unsafe_php_pattern' => 'PHPファイルに許可されていないコードパターンが含まれています (:file)。',
'manifest_not_found' => 'ZIP内に language-pack.json ファイルが見つかりません。',
'manifest_invalid_json' => 'language-pack.json の JSON 形式が正しくありません。',
'php_outside_backend' => 'backend/ ディレクトリ外に PHP ファイルが含まれています (:file)。',
'unsafe_php_pattern' => 'PHP ファイルに許可されていないコードパターンが含まれています (:file)。',
'core_locale_missing' => 'コア言語パック(:locale)を先に有効化する必要があります。',
'target_not_installed' => '対象:scope (:target)がインストールされていません。',
'target_inactive' => '対象:scope (:target)が無効状態です。先に有効化してください。',
'target_version_too_old' => '対象:scope (:target)バージョンが要件(:constraint)を満たしていません。',
'target_not_installed' => '対象 :scope (:target) がインストールされていません。',
'target_inactive' => '対象 :scope (:target) は無効化されています。先に有効化してください。',
'target_version_too_old' => '対象 :scope (:target) のバージョンが要件(:constraint)を満たしていません。',
'downgrade_blocked' => 'ダウングレードがブロックされました (:from → :to)。',
'protected_pack' => '保護された言語パックは無効化·削除できません。',
'download_failed' => 'URL(:url)から言語パックをダウンロードできませんでした。',
'download_failed' => 'URL(:url) から言語パックをダウンロードできませんでした: :error',
'download_url_not_public' => '内部ネットワークアドレス(プライベートIP·localhostなど)からは言語パックをダウンロードできません。外部からアクセス可能なhttpsアドレスを使用してください。',
'checksum_mismatch' => 'チェックサムが一致しません。',
'update_no_source' => 'アップデートソース情報がありません (GitHubソース言語パックのみアップデート可能)。',
'update_already_latest' => 'すでに最新バージョンです。',
'preview_invalid_manifest' => 'manifestファイルが無効なためプレビューに失敗しました。',
'operation_in_progress' => '言語パック(:identifier)で別の処理が進行中です。完了後に再試行してください。',
'update_no_source' => 'アップデートソース情報がありません(GitHubソース言語パックのみアップデート可能)。',
'update_already_latest' => '既に最新バージョンです。',
'preview_invalid_manifest' => 'manifestファイルが有効でなく、プレビューに失敗しました。',
'operation_in_progress' => '言語パック(:identifier)で別のタスクが処理中です。完了後に再度お試しください。',
'bundled_not_found' => 'バンドルディレクトリ(lang-packs/_bundled/:identifier)が存在しません。',
'slot_conflict' => '同じスロットにすでにアクティブな言語パック(:current)があります。(:target)に置き換えるには確認が必要です。',
'directory_not_writable' => '言語パックインストールディレクトリ(:path)への書き込み権限がありません。Webサーバーユーザー(例: www-data)に書き込み権限を付与してください。(例: sudo chgrp www-data :path && sudo chmod g+w :path)',
'identifier_not_found' => '言語パックが見つかりません (:identifier)。',
'slot_conflict' => '同じスロットに既にアクティブな言語パック(:current)があります。(:target)に置き換えるには確認が必要です。',
'directory_not_writable' => '言語パックインストールディレクトリ(:path)に書き込み権限がありません。Webサーバーユーザー(例: www-data)に書き込み権限を付与してください。(例: sudo chgrp www-data :path && sudo chmod g+w :path)',
'identifier_not_found' => '言語パックが見つかりません(:identifier)。',
'unsupported_source' => 'サポートされていないソース(:source) — bundled、github、urlのいずれかを使用してください。',
'download_url_not_public' => '内部ネットワークアドレス(プライベートIP·localhost等)では言語パックをダウンロードできません。外部からアクセス可能なhttpsアドレスを使用してください。',
],
'validation' => [
'file_required' => '言語パックZIPファイルをアップロードしてください。',
'file_invalid' => 'アップロードされたファイルが有効ではありません。',
'file_too_large' => 'ファイルサイズは10MBを超えることはできません。',
'file_not_zip' => 'ZIP形式のファイルのみアップロードできます。',
'github_url_required' => 'GitHubURLを入力してください。',
'github_url_required' => 'GitHubのURLを入力してください。',
'github_url_invalid' => '正しいURLを入力してください。',
'github_url_format' => 'GitHubリポジトリURL形式である必要があります (例: https://github.com/owner/repo)。',
'github_url_format' => 'GitHubリポジトリURLの形式である必要があります(例: https://github.com/owner/repo)。',
'url_required' => 'ダウンロードURLを入力してください。',
'url_invalid' => '正しいURLを入力してください。',
'checksum_invalid' => 'SHA-256チェックサムは64文字の16進数である必要があります。',
'identifier_required' => '言語パック識別子を入力してください。',
'identifier_invalid' => '識別子は英数字·ピリオド·アンダースコア·ハイフンのみ許可されます。',
'identifier_invalid' => '識別子は英文/数字/ドット/アンダースコア/ハイフンのみが許可されます。',
],
];
@@ -2,5 +2,5 @@
return [
'version_warning_title' => '互換性警告',
'version_incompatible' => 'オーバーライド":source"がモジュールバージョンと互換性がありません (要求: :constraint、現在: :current_version)',
'version_incompatible' => 'オーバーライド":source"がモジュールバージョンと互換性がありません(必須: :constraint、現在: :current_version)',
];
@@ -2,6 +2,6 @@
return [
'title' => 'システムメンテナンス中',
'message' => '現在システムアップデートが進行中です。',
'description' => 'しばらく後に再度お試しください。ご不便をおかけして申し訳ございません。',
'message' => '現在、システムアップデートが実施中です。',
'description' => 'しばらく経ってからもう一度お試しください。ご不便をおかけして申し訳ございません。',
];
@@ -13,16 +13,16 @@ return [
'delete_success' => 'メニューが正常に削除されました。',
'delete_failed' => 'メニューの削除に失敗しました。',
'delete_error' => 'メニュー削除中にエラーが発生しました。',
'order_update_success' => 'メニュー順序が正常にアップデートされました。',
'order_update_failed' => 'メニュー順序のアップデートに失敗しました。',
'order_update_success' => 'メニューの順序が正常にアップデートされました。',
'order_update_failed' => 'メニューの順序アップデートに失敗しました。',
'slug_already_exists' => '既に使用中のスラッグです。',
'parent_menu_not_found' => '存在しない親メニューです。',
'cannot_set_self_as_parent' => '自分自身を親として設定することはできません。',
'cannot_delete_menu_with_children' => '子メニューがあるメニューは削除できません。',
'cannot_set_self_as_parent' => '自身を親として設定することはできません。',
'cannot_delete_menu_with_children' => '子メニューを持つメニューは削除できません。',
'validation' => [
'is_active_boolean' => '有効化ステータスはtrue/false値である必要があります。',
'sort_by_invalid' => '並べ替え基準が有効ではありません。',
'sort_order_invalid' => '並べ替え順序が有効ではありません。',
'sort_by_invalid' => 'ソート基準が有効ではありません。',
'sort_order_invalid' => 'ソート順序が有効ではありません。',
'filters_array' => 'フィルターは配列形式である必要があります。',
'filters_max' => 'フィルターは最大10個まで指定できます。',
'filter_field_required' => 'フィルターフィールドは必須です。',
@@ -2,25 +2,27 @@
return [
'not_found' => 'モジュール :module が見つかりません。',
'dependency_not_active' => '依存関係モジュール :dependency がインストールされていないか、有効化されていません。',
'dependency_not_active' => '依存モジュール :dependency がインストールされていないか、有効化されていません。',
'fetch_success' => 'モジュールを正常にインポートしました。',
'fetch_failed' => 'モジュールのインポートに失敗しました。',
'fetch_failed' => 'モジュールのインポートに失敗しました: :error',
'install_success' => 'モジュールが正常にインストールされました。',
'install_failed' => 'モジュールのインストールに失敗しました。',
'preview_success' => 'マニフェストのプレビューが完了しました。',
'preview_failed' => 'マニフェストのプレビューに失敗しました。',
'install_failed' => 'モジュールのインストールに失敗しました: :error',
'preview_success' => 'manifestのプレビューが完了しました。',
'preview_failed' => 'manifestのプレビューに失敗しました: :error',
'uninstall_success' => 'モジュールが正常に削除されました。',
'uninstall_failed' => 'モジュールの削除に失敗しました。',
'uninstall_failed' => 'モジュールの削除に失敗しました: :error',
'activate_success' => 'モジュールが正常に有効化されました。',
'activate_failed' => 'モジュールの有効化に失敗しました。',
'activate_failed' => 'モジュールの有効化に失敗しました: :error',
'deactivate_success' => 'モジュールが正常に無効化されました。',
'deactivate_failed' => 'モジュールの無効化に失敗しました。',
'deactivate_failed' => 'モジュールの無効化に失敗しました: :error',
'update_success' => 'モジュールが正常にアップデートされました。',
'update_failed' => 'モジュールのアップデートに失敗しました。',
'update_failed' => 'モジュールのアップデートに失敗しました: :error',
'refresh_layouts_success' => 'モジュールレイアウトが正常に更新されました。',
'refresh_layouts_failed' => 'モジュールレイアウトの更新に失敗しました。',
'refresh_layouts_failed' => 'モジュールレイアウトの更新に失敗しました: :error',
'uninstall_info_success' => 'モジュール削除情報を正常に取得しました。',
'uninstall_info_failed' => 'モジュール削除情報の取得に失敗しました。',
'uninstall_info_failed' => 'モジュール削除情報の取得に失敗しました: :error',
'dependent_templates_success' => '依存テンプレート情報を正常に取得しました。',
'dependent_templates_failed' => '依存テンプレート情報の取得に失敗しました: :error',
'license_not_found' => 'モジュールライセンスファイルが見つかりません。',
'status' => [
'active' => '有効',
@@ -30,12 +32,10 @@ return [
],
'errors' => [
'module_class_not_found' => 'モジュールクラスが見つかりません。',
'migration_failed' => 'マイグレーション実行に失敗しました。',
'dependency_check_failed' => '依存関係の確認に失敗しました。',
'migration_failed' => 'マイグレーション実行に失敗しました: :error',
'dependency_check_failed' => '依存性確認に失敗しました: :error',
'database_error' => 'データベースエラーが発生しました。',
'module_not_found' => 'モジュール :name が見つかりません。',
'module_not_active' => 'モジュール :name は有効化されていません。',
],
'dependent_templates_success' => '依存テンプレート情報を正常に取得しました。',
'dependent_templates_failed' => '依存テンプレート情報の取得に失敗しました: :error',
];
@@ -23,58 +23,58 @@ return [
'uninstalled' => '未インストール',
],
'no_modules' => 'モジュールがありません。',
'summary' => '合計 :total個 モジュール (インストール済み: :installed、有効化: :active)',
'summary' => '合計 :total 個のモジュール (インストール済み: :installed、有効化: :active)',
'invalid_status' => '無効なステータスです。(installed、uninstalled、active、inactive)',
],
'install' => [
'success' => 'モジュール ":module" インストール完了',
'success' => 'モジュール ":module" のインストールが完了しました',
'vendor' => 'ベンダー: :vendor',
'version' => 'バージョン: :version',
'roles_created' => ':count個 ロール 作成されました',
'permissions_created' => ':count個 権限 作成されました',
'menus_created' => ':count個 メニュー 作成されました',
'roles_created' => ':count 個のロールが作成されました',
'permissions_created' => ':count 個の権限が作成されました',
'menus_created' => ':count 個のメニューが作成されました',
'already_installed' => 'モジュール ":module" は既にインストールされています。',
'force_reinstall' => 'モジュール ":module" を強制的に再インストールします (アクティブディレクトリを上書き)...',
],
'activate' => [
'success' => 'モジュール ":module" 有効化完了',
'not_installed' => 'モジュール ":module" はインストールされていません。',
'success' => 'モジュール ":module" の有効化が完了しました',
'not_installed' => 'モジュール ":module" がインストールされていません。',
'already_active' => 'モジュール ":module" は既に有効化されています。',
'layouts_registered' => ':count個 レイアウト 登録されました',
'layouts_registered' => ':count 個のレイアウトが登録されました',
],
'deactivate' => [
'success' => 'モジュール ":module" 無効化完了',
'not_installed' => 'モジュール ":module" はインストールされていません。',
'success' => 'モジュール ":module" の無効化が完了しました',
'not_installed' => 'モジュール ":module" がインストールされていません。',
'not_active' => 'モジュール ":module" は有効化されていません。',
'warning' => 'モジュールのルートと機能が無効化されました。',
'layouts_deleted' => ':count個 レイアウト 削除されました',
'layouts_deleted' => ':count 個のレイアウトが削除されました',
],
'uninstall' => [
'success' => 'モジュール ":module" 削除完了',
'roles_deleted' => ':count個 ロール 削除されました',
'permissions_deleted' => ':count個 権限 削除されました',
'menus_deleted' => ':count個 メニュー 削除されました',
'layouts_deleted' => ':count個 レイアウト 削除されました',
'confirm_prompt' => 'モジュール ":module" を削除しますか?',
'success' => 'モジュール ":module" の削除が完了しました',
'roles_deleted' => ':count 個のロールが削除されました',
'permissions_deleted' => ':count 個の権限が削除されました',
'menus_deleted' => ':count 個のメニューが削除されました',
'layouts_deleted' => ':count 個のレイアウトが削除されました',
'confirm_prompt' => 'モジュール ":module" を削除してもよろしいですか?',
'confirm_details' => [
'roles' => '- :count個 のロールが削除されます。',
'permissions' => '- :count個 の権限が削除されます。',
'menus' => '- :count個 のメニューが削除されます。',
'layouts' => '- :count個 のレイアウトが削除されます。',
'roles' => '- :count 個のロールが削除されます。',
'permissions' => '- :count 個の権限が削除されます。',
'menus' => '- :count 個のメニューが削除されます。',
'layouts' => '- :count 個のレイアウトが削除されます。',
'data' => '- すべてのモジュールデータが削除されます。',
],
'confirm_question' => '本当に削除しますか?',
'aborted' => 'モジュール削除がキャンセルされました。',
'not_installed' => 'モジュール ":module" はインストールされていません。',
'confirm_question' => '本当に削除してもよろしいですか?',
'aborted' => 'モジュールの削除がキャンセルされました。',
'not_installed' => 'モジュール ":module" がインストールされていません。',
],
'cache_clear' => [
'clearing_all' => 'すべてのモジュールキャッシュを削除します...',
'clearing_single' => 'モジュール ":module" キャッシュを削除します...',
'success_all' => 'モジュールキャッシュ削除完了 (:count個 項目)',
'success_single' => 'モジュール ":module" キャッシュ削除完了 (:count個 項目)',
'clearing_single' => 'モジュール ":module" のキャッシュを削除します...',
'success_all' => 'モジュールキャッシュの削除が完了しました (:count 個の項目)',
'success_single' => 'モジュール ":module" のキャッシュ削除が完了しました (:count 個の項目)',
],
'check_updates' => [
'not_installed' => 'モジュール ":module" はインストールされていません。',
'not_installed' => 'モジュール ":module" がインストールされていません。',
'no_installed' => 'インストール済みモジュールがありません。',
'update_available' => 'アップデート可能',
'up_to_date' => '最新',
@@ -85,122 +85,122 @@ return [
'source' => 'ソース',
'status' => 'ステータス',
],
'summary' => '合計 :total個 確認 (アップデート可能: :updates個)',
'summary' => '合計 :total 個を確認 (アップデート可能: :updates 個)',
'single_up_to_date' => 'モジュール ":module" は最新バージョンです。(v:version)',
'single_update_available' => 'モジュール ":module" アップデート可能: v:current → v:latest (:source)',
'single_update_available' => 'モジュール ":module" はアップデート可能です: v:current → v:latest (:source)',
],
'update' => [
'success' => 'モジュール ":module" アップデート完了',
'success' => 'モジュール ":module" のアップデートが完了しました',
'version_change' => 'v:from → v:to',
'no_update' => 'モジュール ":module" に利用可能なアップデートはありません。',
'not_installed' => 'モジュール ":module" はインストールされていません。',
'not_installed' => 'モジュール ":module" がインストールされていません。',
'current_version' => '現在のバージョン: :version',
'latest_version' => '最新バージョン: :version',
'update_source' => 'アップデートソース: :source',
'confirm_question' => 'アップデートを実行しますか?',
'confirm_question' => 'アップデートを進めてもよろしいですか?',
'aborted' => 'アップデートがキャンセルされました。',
'backup_restored' => 'バックアップから以前のバージョンが復元されました。',
'force_mode' => '強制アップデートモード: バージョン比較をスキップして再インストールします。',
'layout_strategy' => 'レイアウト戦略: :strategy',
'modified_extensions_warning' => '管理者が編集したレイアウト拡張 :count件が上書きされます。',
'modified_extension_item' => ' - :target (出典: :source)',
'modified_extensions_warning' => '管理者が編集したレイアウト拡張 :count 件が上書きされます。',
'modified_extension_item' => ' - :target (ソース: :source)',
],
],
'composer_install' => [
'start' => 'モジュール ":module" の Composer 依存関係をインストールします...',
'success' => 'モジュール ":module" の Composer 依存関係 インストール完了',
'success' => 'モジュール ":module" の Composer 依存関係のインストールが完了しました',
'no_dependencies' => 'モジュール ":module" にインストールする Composer 依存関係はありません。',
'failed' => 'モジュール ":module" の Composer 依存関係 インストールに失敗しました。',
'failed' => 'モジュール ":module" の Composer 依存関係のインストールに失敗しました。',
'summary' => '📊 結果: 成功 :success個、スキップ :skip個、失敗 :fail個',
],
'update_success' => 'モジュール ":module" が :version バージョンにアップデートされました。',
'update_failed' => 'モジュールアップデートに失敗しました: :error',
'update_hook_failed' => 'モジュール アップデート フック実行に失敗しました。',
'update_success' => 'モジュール ":module"が :version バージョンにアップデートされました。',
'update_failed' => 'モジュールのアップデートに失敗しました: :error',
'update_hook_failed' => 'モジュールアップデートフック実行に失敗しました: :error',
'no_update_available' => 'アップデートする内容がありません。',
'check_updates_success' => 'アップデート確認が完了しました。',
'check_updates_failed' => 'アップデート確認に失敗しました。',
'check_modified_layouts_success' => '修正されたレイアウト確認が完了しました。',
'check_modified_layouts_failed' => '修正されたレイアウト確認に失敗しました: :error',
'not_installed' => 'モジュール":module"はインストールされていません。',
'pending_not_found' => 'インストール待ちリストでモジュール":module"が見つかりません。',
'already_exists' => 'モジュール":module"は既に存在します。',
'move_failed' => 'モジュール":module"の移動に失敗しました。',
'deactivate_warning' => 'このモジュールに依存する有効なテンプレートがあります。',
'installation_failed' => 'モジュール インストールに失敗しました: :error',
'activation_failed' => 'モジュール 有効化に失敗しました: :error',
'deactivation_failed' => 'モジュール 無効化に失敗しました: :error',
'uninstallation_failed' => 'モジュール 削除に失敗しました: :error',
'refresh_layouts_failed' => 'レイアウト 更新に失敗しました: :error',
'check_updates_failed' => 'アップデート確認に失敗しました: :error',
'check_modified_layouts_success' => '変更されたレイアウト確認が完了しました。',
'check_modified_layouts_failed' => '変更されたレイアウト確認に失敗しました: :error',
'not_installed' => 'モジュール ":module"はインストールされていません。',
'pending_not_found' => 'インストール待機リストでモジュール ":module"が見つかりません。',
'already_exists' => 'モジュール ":module"は既に存在します。',
'move_failed' => 'モジュール ":module"の移動に失敗しました: :error',
'deactivate_warning' => 'このモジュールに依存するアクティブなテンプレートがあります。',
'installation_failed' => 'モジュールのインストールに失敗しました: :error',
'activation_failed' => 'モジュールの有効化に失敗しました: :error',
'deactivation_failed' => 'モジュールの無効化に失敗しました: :error',
'uninstallation_failed' => 'モジュールの削除に失敗しました: :error',
'refresh_layouts_failed' => 'レイアウト更新に失敗しました: :error',
'warnings' => [
'has_dependent_templates' => 'このモジュールに依存する有効化されたテンプレートがあります。無効化するには強制オプションを使用してください。',
'has_dependents' => 'このモジュールに依存する有効化された拡張があります。',
'deactivation_warning' => '無効化すると次のテンプレートが影響を受けます:',
'deactivation_warning' => '無効化すると、次のテンプレートが影響を受けます:',
'confirm_deactivation' => 'それでも無効化しますか?',
'missing_dependencies' => 'このモジュールを有効化するには次の依存性が必要です。',
'missing_dependencies' => 'このモジュールを有効化するには、次の依存関係が必要です。',
],
'activate_warning' => 'モジュール 有効化に必要な依存性が満たされていません。',
'deactivate_warning_extended' => 'このモジュールに依存する有効化された拡張(テンプレート、モジュール、プラグイン)があります。',
'activate_warning' => 'モジュール有効化に必要な依存関係が満たされていません。',
'deactivate_warning_extended' => 'このモジュールに依存する有効化された拡張 (テンプレート、モジュール、プラグイン) があります。',
'errors' => [
'force_update_no_source' => ':moduleを強制アップデートするソースが見つかりません。バンドルおよびGitHub URLの両方がありません。',
'downgrade_blocked' => 'ダウングレードがブロックされました(:from → :to)。意図的なダウングレードは--forceオプションを使用してください。',
'zip_open_failed' => 'ZIPファイルを開くことができません。',
'module_json_not_found' => 'module.jsonファイルが見つかりません。',
'module_json_invalid' => 'module.jsonファイル形式が正しくありません。',
'identifier_missing' => 'モジュール識別子が不足しています。',
'force_update_no_source' => ':module を強制アップデートするソースが見つかりません。バンドルと GitHub URL の両方がありません。',
'downgrade_blocked' => 'ダウングレードがブロックされました (:from → :to)。意図的なダウングレードは --force オプションを使用してください。',
'zip_open_failed' => 'ZIP ファイルを開くことができません: :error',
'module_json_not_found' => 'module.json ファイルが見つかりません。',
'module_json_invalid' => 'module.json ファイル形式が正しくありません。',
'identifier_missing' => 'モジュール識別子がありません。',
'already_installed' => '既にインストールされているモジュールです。',
'install_failed' => 'モジュール インストールに失敗しました。',
'github_url_invalid' => '無効なGitHub URLの形式です。',
'github_download_failed' => 'GitHubからのダウンロードに失敗しました。',
'install_failed' => 'モジュールのインストールに失敗しました: :error',
'github_url_invalid' => '無効な GitHub URL 形式です。',
'github_download_failed' => 'GitHub からのダウンロードに失敗しました: :error',
'github_repo_not_found' => 'GitHub リポジトリが見つかりません。',
'module_not_found' => 'モジュール:nameが見つかりません。',
'module_not_active' => 'モジュール:nameは有効化されていません。',
'not_found' => 'モジュール:moduleが見つかりません。',
'module_not_found' => 'モジュール :name が見つかりません。',
'module_not_active' => 'モジュール :name は有効化されていません。',
'not_found' => 'モジュール :module が見つかりません。',
'file_not_found' => 'ファイルが見つかりません。',
'file_type_not_allowed' => '許可されていないファイル形式です。',
'unknown_error' => '不明なエラーが発生しました。',
'invalid_permission_structure' => 'モジュール":identifier"の権限構造が正しくありません: :reason',
'invalid_translation_path' => 'モジュール":identifier"の言語パック ファイルパスが正しくありません。":wrong_path"の代わりに":correct_path"を使用する必要があります。',
'seo_variable_conflict' => 'モジュール":identifier"のSEO変数名が既存の拡張と競合します: :reason',
'layout_validation_json_error' => 'レイアウト ファイル:fileのJSON解析に失敗しました: :error',
'layout_validation_missing_name' => 'レイアウト ファイル:fileにlayout_nameが不足しています。',
'layout_validation_partial_error' => 'レイアウト ファイル:fileのpartial処理に失敗しました: :error (partial: :partial)',
'layout_validation_validation_failed' => 'モジュール:identifierのレイアウト バリデーションに失敗しました。:count個のエラーが見つかりました。',
'operation_in_progress' => 'モジュール":name"は現在:statusの状態です。処理が完了するまでお待ちください。',
'github_api_failed' => 'GitHub API呼び出しに失敗しました。',
'invalid_github_url' => '無効なGitHub URLです。',
'zip_url_not_found' => 'ZIPダウンロード URLが見つかりません。',
'download_failed' => 'モジュール":module":versionバージョンのダウンロードに失敗しました。',
'zip_extract_failed' => 'ZIP展開に失敗しました。',
'extracted_dir_not_found' => '展開されたディレクトリが見つかりません。',
'reload_failed' => 'モジュール 再ロードに失敗しました。',
'delete_directory_failed' => 'モジュール ディレクトリの削除に失敗しました。',
'update_failed' => 'モジュール":module"のアップデートに失敗しました: :error',
'invalid_layout_strategy' => '無効なレイアウト戦略です。(overwriteまたはkeepのみ許可)',
'invalid_vendor_mode' => '無効なVendor インストール モードです。(auto、composer、bundledのみ許可)',
'invalid_permission_structure' => 'モジュール ":identifier"の権限構造が正しくありません: :reason',
'invalid_translation_path' => 'モジュール ":identifier"の言語ファイルパスが正しくありません。":wrong_path" の代わりに ":correct_path"を使用する必要があります。',
'seo_variable_conflict' => 'モジュール ":identifier"の SEO 変数名が既存の拡張と競合しています: :reason',
'layout_validation_json_error' => 'レイアウトファイル :file の JSON パースに失敗しました: :error',
'layout_validation_missing_name' => 'レイアウトファイル :file に layout_name がありません。',
'layout_validation_partial_error' => 'レイアウトファイル :file の partial 処理に失敗しました: :error (partial: :partial)',
'layout_validation_validation_failed' => 'モジュール :identifier のレイアウト検証に失敗しました。:count 個のエラーが見つかりました: :error',
'operation_in_progress' => 'モジュール ":name"は現在 :status 状態です。処理が完了するまでお待ちください。',
'github_api_failed' => 'GitHub API 呼び出しに失敗しました: :error',
'invalid_github_url' => '無効な GitHub URL です。',
'zip_url_not_found' => 'ZIP ダウンロード URL が見つかりません。',
'download_failed' => 'モジュール ":module" :version バージョンのダウンロードに失敗しました: :error',
'zip_extract_failed' => 'ZIP 解凍に失敗しました: :error',
'extracted_dir_not_found' => '解凍されたディレクトリが見つかりません。',
'reload_failed' => 'モジュールの再読み込みに失敗しました: :error',
'delete_directory_failed' => 'モジュールディレクトリの削除に失敗しました: :error',
'update_failed' => 'モジュール ":module"のアップデートに失敗しました: :error',
'invalid_layout_strategy' => '無効なレイアウト戦略です。(overwrite または keep のみ許可)',
'invalid_vendor_mode' => '無効な Vendor インストールモードです。(auto、composer、bundled のみ許可)',
],
'validation' => [
'name_required' => 'モジュール名は必須です。',
'name_string' => 'モジュール名は文字列である必要があります。',
'name_max' => 'モジュール名は255字を超えることはできません。',
'search_max' => '検索語は255字を超えることはできません。',
'filters_max' => '検索条件は最大10個までのみ指定できます。',
'name_max' => 'モジュール名は 255 文字を超えることができません。',
'search_max' => '検索語は 255 文字を超えることができません。',
'filters_max' => '検索条件は最大 10 個までのみ指定できます。',
'filter_field_required' => '検索フィールドは必須です。',
'filter_field_invalid' => '無効な検索フィールドです。',
'filter_value_required' => '検索値は必須です。',
'filter_value_max' => '検索値は255字を超えることはできません。',
'filter_value_max' => '検索値は 255 文字を超えることができません。',
'filter_operator_invalid' => '無効な検索演算子です。',
'status_invalid' => '無効なステータスです。',
'per_page_min' => 'ページあたりの項目数は最小1です。',
'per_page_max' => 'ページあたりの項目数は最大100です。',
'page_min' => 'ページ番号は最小1です。',
'file_required' => 'モジュール ファイルは必須です。',
'status_invalid' => '無効な状態です。',
'per_page_min' => '1 ページあたりの項目数は最小 1 個です。',
'per_page_max' => '1 ページあたりの項目数は最大 100 個です。',
'page_min' => 'ページ番号は最小 1 です。',
'file_required' => 'モジュールファイルは必須です。',
'file_invalid' => '有効なファイルではありません。',
'file_must_be_zip' => 'モジュール ファイルはZIP形式である必要があります。',
'file_max_size' => 'モジュール ファイル サイズは:sizeMBを超えることはできません。',
'file_must_be_zip' => 'モジュールファイルは ZIP 形式である必要があります。',
'file_max_size' => 'モジュールファイルサイズは :sizeMB を超えることができません。',
'github_url_required' => 'GitHub URLは必須です。',
'github_url_invalid' => '有効なURL形式ではありません。',
'github_url_format' => 'GitHub リポジトリURL形式が正しくありません。',
'with_max' => 'withパラメータは最大10個まで指定できます。',
'with_invalid' => '無効なwith関係です。',
'with_max' => 'with パラメータは最大10個まで指定できます。',
'with_invalid' => '有効でないwith 関係です。',
],
];
@@ -1,49 +1,50 @@
<?php
return [
'definition_list_success' => '通知定義一覧を取得しました。',
'definition_list_failed' => '通知定義一覧の取得に失敗しました。',
'definition_list_success' => '通知定義リストを取得しました。',
'definition_list_failed' => '通知定義リストの取得に失敗しました。',
'definition_show_success' => '通知定義を取得しました。',
'definition_show_failed' => '通知定義の取得に失敗しました。',
'definition_updated' => '通知定義が編集されました。',
'definition_update_failed' => '通知定義の編集に失敗しました。',
'definition_toggled' => '通知定義のアクティブ状態が変更されました。',
'definition_toggle_failed' => '通知定義のアクティブ状態の変更に失敗しました。',
'definition_toggled' => '通知定義の有効状態が変更されました。',
'definition_toggle_failed' => '通知定義の有効状態変更に失敗しました。',
'definition_not_found' => '通知定義が見つかりません。',
'definition_reset' => '通知定義のすべてのテンプレートがデフォルト値に復元されました。',
'definition_reset_failed' => '通知定義の初期化に失敗しました。',
'channel_skipped_no_template' => ':channel チャネルに :type タイプのアクティブなテンプレートがないため、配信をスキップしました。',
'channel_disabled_by_extension' => '拡張設定でそのチャネルが無効化されているため、配信をスキップしました。',
'channel_skipped_no_template' => ':channel チャネルに :type タイプのアクティブテンプレートがないため、送信をスキップしました。',
'channel_disabled_by_extension' => '拡張環境設定でそのチャネルが無効化されているため、送信をスキップしました。',
'channel_guest_not_allowed' => 'そのチャネルは非会員送信を許可していないため、送信をスキップしました。',
'template_updated' => '通知テンプレートが編集されました。',
'template_update_failed' => '通知テンプレートの編集に失敗しました。',
'template_toggled' => '通知テンプレートのアクティブ状態が変更されました。',
'template_toggle_failed' => '通知テンプレートのアクティブ状態の変更に失敗しました。',
'template_toggled' => '通知テンプレートの有効状態が変更されました。',
'template_toggle_failed' => '通知テンプレートの有効状態変更に失敗しました。',
'template_reset' => '通知テンプレートがデフォルト値に復元されました。',
'template_reset_failed' => '通知テンプレートのデフォルト値復元に失敗しました。',
'template_inactive' => '該当チャネルの通知テンプレートは無効化状態です。',
'template_inactive' => '該当チャネルの通知テンプレートは無効状態です。',
'template_not_found' => '該当チャネルの通知テンプレートが見つかりません。',
'default_data_not_found' => 'デフォルトテンプレートデータが見つかりません。',
'preview_success' => 'プレビューを生成しました。',
'preview_failed' => 'プレビューの生成に失敗しました。',
'channels_success' => '通知チャネル一覧を取得しました。',
'channels_failed' => '通知チャネル一覧の取得に失敗しました。',
'preview_success' => 'プレビューを作成しました。',
'preview_failed' => 'プレビューの作成に失敗しました。',
'channels_success' => '通知チャネルリストを取得しました。',
'channels_failed' => '通知チャネルリストの取得に失敗しました。',
'definition' => '通知定義',
'subject' => '件名',
'body' => '本文',
'readiness' => [
'mail_from_address_not_configured' => '送信者メールアドレスが設定されていません。',
'mail_smtp_host_empty' => 'SMTPホストが設定されていません。',
'mail_smtp_port_empty' => 'SMTPポートが設定されていません。',
'mail_mailgun_domain_empty' => 'Mailgunドメインが設定されていません。',
'mail_mailgun_secret_empty' => 'Mailgunシークレットが設定されていません。',
'mail_ses_key_empty' => 'SESキーが設定されていません。',
'mail_ses_secret_empty' => 'SESシークレットが設定されていません。',
'mail_smtp_host_empty' => 'SMTP ホストが設定されていません。',
'mail_smtp_port_empty' => 'SMTP ポートが設定されていません。',
'mail_mailgun_domain_empty' => 'Mailgun ドメインが設定されていません。',
'mail_mailgun_secret_empty' => 'Mailgun シークレットが設定されていません。',
'mail_ses_key_empty' => 'SES キーが設定されていません。',
'mail_ses_secret_empty' => 'SES シークレットが設定されていません。',
'database_table_missing' => '通知テーブルが存在しません。',
'unknown' => 'チャネル設定が完了していません。',
],
'user' => [
'list_success' => '通知一覧を取得しました。',
'list_failed' => '通知一覧の取得に失敗しました。',
'list_success' => '通知リストを取得しました。',
'list_failed' => '通知リストの取得に失敗しました。',
'unread_count_success' => '未読通知数を取得しました。',
'unread_count_failed' => '未読通知数の取得に失敗しました。',
'read_success' => '通知を既読にしました。',
@@ -53,19 +54,18 @@ return [
'delete_success' => '通知が削除されました。',
'delete_failed' => '通知の削除に失敗しました。',
'delete_all_success' => 'すべての通知が削除されました。',
'delete_all_failed' => '全件削除に失敗しました。',
'delete_all_failed' => '全件通知削除に失敗しました。',
'not_found' => '通知が見つかりません。',
],
'channels' => [
'core_default' => 'コアの基本チャネル',
'core_default' => 'コアデフォルトチャネル',
'mail' => [
'name' => 'メール',
'description' => 'メールで通知を送信',
],
'database' => [
'name' => 'サイト内通知',
'description' => 'サイト内の通知センターに表示',
'description' => 'サイト内通知センターに表示',
],
],
'channel_guest_not_allowed' => '該当チャネルは非会員発送を許可していないため、発送をスキップしました。',
];
@@ -1,8 +1,8 @@
<?php
return [
'list_success' => '通知発送履歴を閲覧しました。',
'list_failed' => '通知発送履歴の閲覧に失敗しました。',
'list_success' => '通知発送履歴を取得しました。',
'list_failed' => '通知発送履歴の取得に失敗しました。',
'delete_success' => '通知発送履歴が削除されました。',
'delete_failed' => '通知発送履歴の削除に失敗しました。',
'bulk_delete_success' => '選択された通知発送履歴が削除されました。',
@@ -1,119 +1,121 @@
<?php
return [
'list_success' => 'プラグインリストの取得に成功しました。',
'list_failed' => 'プラグインリストの取得に失敗しました。',
'fetch_success' => 'プラグイン情報の取得に成功しました。',
'fetch_failed' => 'プラグイン情報の取得に失敗しました。',
'list_success' => 'プラグインのリストを正常に取得しました。',
'list_failed' => 'プラグインのリストの取得に失敗しました: :error',
'fetch_success' => 'プラグインの情報を正常に取得しました。',
'fetch_failed' => 'プラグインの情報の取得に失敗しました: :error',
'not_found' => 'プラグイン :plugin が見つかりません。',
'install_success' => 'プラグインが正常にインストールされました。',
'install_failed' => 'プラグインのインストールに失敗しました。',
'preview_success' => 'マニフェストプレビューが完了しました。',
'preview_failed' => 'マニフェストプレビューに失敗しました。',
'install_validation_failed' => 'プラグインのインストール検証に失敗しました。',
'install_success' => 'プラグインは正常にインストールされました。',
'install_failed' => 'プラグインのインストールに失敗しました: :error',
'preview_success' => 'manifest プレビューが完了しました。',
'preview_failed' => 'manifest プレビューに失敗しました: :error',
'install_validation_failed' => 'プラグイン インストール バリデーションに失敗しました: :error',
'install_error' => 'プラグインのインストール中にエラーが発生しました。',
'uninstall_success' => 'プラグインが正常に削除されました。',
'uninstall_failed' => 'プラグインの削除に失敗しました。',
'uninstall_validation_failed' => 'プラグインの削除検証に失敗しました。',
'uninstall_success' => 'プラグインは正常に削除されました。',
'uninstall_failed' => 'プラグインの削除に失敗しました: :error',
'uninstall_validation_failed' => 'プラグイン削除 バリデーションに失敗しました: :error',
'uninstall_error' => 'プラグインの削除中にエラーが発生しました。',
'activate_success' => 'プラグインが正常に有効化されました。',
'activate_failed' => 'プラグインの有効化に失敗しました。',
'activate_validation_failed' => 'プラグインの有効化検証に失敗しました。',
'activate_success' => 'プラグインは正常に有効化されました。',
'activate_failed' => 'プラグインの有効化に失敗しました: :error',
'activate_validation_failed' => 'プラグイン有効化 バリデーションに失敗しました: :error',
'activate_error' => 'プラグインの有効化中にエラーが発生しました。',
'deactivate_success' => 'プラグインが正常に無効化されました。',
'deactivate_failed' => 'プラグインの無効化に失敗しました。',
'deactivate_validation_failed' => 'プラグインの無効化検証に失敗しました。',
'deactivate_success' => 'プラグインは正常に無効化されました。',
'deactivate_failed' => 'プラグインの無効化に失敗しました: :error',
'deactivate_validation_failed' => 'プラグイン無効化 バリデーションに失敗しました: :error',
'deactivate_error' => 'プラグインの無効化中にエラーが発生しました。',
'refresh_layouts_success' => 'プラグインレイアウトが正常に更新されました。',
'refresh_layouts_failed' => 'プラグインレイアウトの更新に失敗しました: :error',
'uninstall_info_success' => 'プラグイン削除情報の閲覧に成功しました。',
'uninstall_info_failed' => 'プラグイン削除情報の閲覧に失敗しました。',
'uninstall_info_success' => 'プラグイン削除情報を正常に取得しました。',
'uninstall_info_failed' => 'プラグイン削除情報の取得に失敗しました: :error',
'dependent_templates_success' => '依存テンプレート情報を正常に取得しました。',
'dependent_templates_failed' => '依存テンプレート情報の取得に失敗しました: :error',
'license_not_found' => 'プラグインライセンスファイルが見つかりません。',
'refresh_layouts_validation_failed' => 'プラグインレイアウト更新検証に失敗しました。',
'refresh_layouts_validation_failed' => 'プラグインレイアウト更新 バリデーションに失敗しました: :error',
'refresh_layouts_error' => 'プラグインレイアウトの更新中にエラーが発生しました。',
'status' => [
'active' => '有効',
'inactive' => '無効',
'updating' => 'アップデート中',
'active' => '有効化',
'inactive' => '無効化',
'updating' => '更新中',
],
'update_success' => 'プラグイン ":plugin" は :version にアップデートされました。',
'update_success' => 'プラグイン ":plugin" は :version バージョンにアップデートされました。',
'update_failed' => 'プラグインのアップデートに失敗しました: :error',
'update_hook_failed' => 'プラグイン更新フックの実行に失敗しました。',
'no_update_available' => '更新する内容がありません。',
'check_updates_success' => '更新確認が完了しました。',
'check_updates_failed' => '更新確認に失敗しました。',
'check_modified_layouts_success' => '変更されたレイアウト確認が完了しました。',
'check_modified_layouts_failed' => '変更されたレイアウト確認に失敗しました: :error',
'update_hook_failed' => 'プラグイン アップデート フック実行に失敗しました: :error',
'no_update_available' => 'アップデートする内容がありません。',
'check_updates_success' => 'アップデート確認が完了しました。',
'check_updates_failed' => 'アップデート確認に失敗しました: :error',
'check_modified_layouts_success' => '更新されたレイアウト確認が完了しました。',
'check_modified_layouts_failed' => '更新されたレイアウト確認に失敗しました: :error',
'not_installed' => 'プラグイン ":plugin" はインストールされていません。',
'pending_not_found' => 'インストール待機リストにプラグイン ":plugin" が見つかりません。',
'already_exists' => 'プラグイン ":plugin" は既に存在します。',
'move_failed' => 'プラグイン ":plugin" の移動に失敗しました。',
'dependency_not_active' => '依存関係 ":dependency" がインストールされていないか、無効化されています。',
'move_failed' => 'プラグイン ":plugin" の移動に失敗しました: :error',
'dependency_not_active' => '依存 ":dependency" はインストールされていないか、無効化されています。',
'installation_failed' => 'プラグインのインストールに失敗しました: :error',
'activation_failed' => 'プラグインの有効化に失敗しました: :error',
'deactivation_failed' => 'プラグインの無効化に失敗しました: :error',
'uninstallation_failed' => 'プラグインの削除に失敗しました: :error',
'warnings' => [
'has_dependent_templates' => 'このプラグインに依存する有効なテンプレートがあります。無効化するには強制オプションを使用してください。',
'has_dependents' => 'このプラグインに依存する有効な拡張があります。',
'deactivation_warning' => '無効化すると、次のテンプレートが影響を受けます:',
'has_dependent_templates' => 'このプラグインに依存する有効化されたテンプレートがあります。無効化するには強制オプションを使用してください。',
'has_dependents' => 'このプラグインに依存する有効化された拡張があります。',
'deactivation_warning' => '無効化すると、以下のテンプレートが影響を受けます:',
'confirm_deactivation' => 'それでも無効化しますか?',
'missing_dependencies' => 'このプラグインを有効化するには、次の依存関係が必要です。',
'missing_dependencies' => 'このプラグインを有効化するには、以下の依存が必要です。',
],
'activate_warning' => 'プラグインの有効化に必要な依存関係が満たされていません。',
'deactivate_warning' => 'このプラグインに依存する有効な拡張があります。',
'deactivate_warning_extended' => 'このプラグインに依存する有効な拡張(テンプレート、モジュール、プラグイン)があります。',
'activate_warning' => 'プラグイン有効化に必要な依存が満たされていません。',
'deactivate_warning' => 'このプラグインに依存する有効化された拡張があります。',
'deactivate_warning_extended' => 'このプラグインに依存する有効化された拡張(テンプレート、モジュール、プラグイン)があります。',
'errors' => [
'force_update_no_source' => ':plugin を強制アップデートするソースが見つかりません。バンドルとGitHub URLがどちらもありません。',
'downgrade_blocked' => 'ダウングレードはブロックされています (:from → :to)。意図的なダウングレードの場合は --force オプションを使用してください。',
'force_update_no_source' => ':plugin を強制アップデートするソースが見つかりません。バンドルと GitHub URL の両方がありません。',
'downgrade_blocked' => 'ダウングレードがブロックされました (:from → :to)。意図的なダウングレードは --force オプションを使用してください。',
'invalid_permission_structure' => 'プラグイン ":identifier" の権限構造が正しくありません: :reason',
'invalid_translation_path' => 'プラグイン ":identifier" の言語ファイルパスが正しくありません。":wrong_path" の代わりに ":correct_path" を使用する必要があります。',
'seo_variable_conflict' => 'プラグイン ":identifier" のSEO変数名が既存の拡張と競合しています: :reason',
'invalid_translation_path' => 'プラグイン ":identifier" の言語ファイルパスが正しくありません。":wrong_path" ではなく ":correct_path" を使用してください。',
'seo_variable_conflict' => 'プラグイン ":identifier" の SEO 変数名が既存の拡張と競合します: :reason',
'plugin_not_found' => 'プラグイン ":name" が見つかりません。',
'plugin_not_active' => 'プラグイン ":name" は有効化されていません。',
'not_found' => 'プラグイン :plugin が見つかりません。',
'file_not_found' => 'ファイルが見つかりません。',
'file_type_not_allowed' => '許可されていないファイル形式です。',
'unknown_error' => '不明なエラーが発生しました。',
'layout_validation_json_error' => 'レイアウトファイル :file のJSON解析に失敗しました: :error',
'layout_validation_missing_name' => 'レイアウトファイル :file に layout_name が不足しています。',
'layout_validation_partial_error' => 'レイアウトファイル :file の部分テンプレート処理に失敗しました: :error (partial: :partial)',
'layout_validation_validation_failed' => 'プラグイン :identifier のレイアウト検証に失敗しました。:count 個のエラーが見つかりました。',
'operation_in_progress' => 'プラグイン ":name" は現在 :status 状態です。作業が完了するまで待ってください。',
'github_api_failed' => 'GitHub API呼び出しに失敗しました。',
'invalid_github_url' => '無効なGitHub URLです。',
'zip_url_not_found' => 'ZIPダウンロードURLが見つかりません。',
'download_failed' => 'プラグイン ":plugin" :version バージョンのダウンロードに失敗しました。',
'zip_extract_failed' => 'ZIP解凍に失敗しました。',
'layout_validation_json_error' => 'レイアウトファイル :file の JSON パース に失敗しました: :error',
'layout_validation_missing_name' => 'レイアウトファイル :file に layout_name がありません。',
'layout_validation_partial_error' => 'レイアウトファイル :file の partial 処理に失敗しました: :error (partial: :partial)',
'layout_validation_validation_failed' => 'プラグイン :identifier のレイアウト バリデーションに失敗しました。:count 個のエラーが見つかりました: :error',
'operation_in_progress' => 'プラグイン ":name" は現在 :status 状態です。作業が完了するまでお待ちください。',
'github_api_failed' => 'GitHub API 呼び出しに失敗しました: :error',
'invalid_github_url' => '無効な GitHub URL です。',
'zip_url_not_found' => 'ZIP ダウンロード URL が見つかりません。',
'download_failed' => 'プラグイン ":plugin" :version バージョンのダウンロードに失敗しました: :error',
'zip_extract_failed' => 'ZIP 解凍に失敗しました: :error',
'extracted_dir_not_found' => '解凍されたディレクトリが見つかりません。',
'reload_failed' => 'プラグインの再読み込みに失敗しました。',
'delete_directory_failed' => 'プラグインディレクトリの削除に失敗しました。',
'update_failed' => 'プラグイン ":plugin" のアップデートに失敗しました: :error',
'reload_failed' => 'プラグイン再読み込みに失敗しました: :error',
'delete_directory_failed' => 'プラグイン ディレクトリ削除に失敗しました: :error',
'update_failed' => 'プラグイン ":plugin" アップデートに失敗しました: :error',
'invalid_layout_strategy' => '無効なレイアウト戦略です。(overwrite または keep のみ許可)',
'invalid_vendor_mode' => '無効な Vendor インストールモードです。(auto、composer、bundled のみ許可)',
'github_url_invalid' => '無効な GitHub URL 形式です。',
'github_download_failed' => 'GitHub からのダウンロードに失敗しました。',
'github_download_failed' => 'GitHub からのダウンロードに失敗しました: :error',
'github_repo_not_found' => 'GitHub リポジトリが見つかりません。',
'zip_open_failed' => 'ZIP ファイルを開くことができません。',
'zip_open_failed' => 'ZIP ファイルを開くことができません: :error',
'plugin_json_not_found' => 'plugin.json ファイルが見つかりません。',
'plugin_json_invalid' => 'plugin.json ファイル形式が正しくありません。',
'identifier_missing' => 'プラグイン識別子が見つかりません。',
'identifier_missing' => 'プラグイン識別子が不足しています。',
'already_installed' => 'プラグイン ":plugin" は既にインストールされています。',
'install_failed' => 'プラグインのインストールに失敗しました。',
'install_failed' => 'プラグイン インストールに失敗しました: :error',
],
'validation' => [
'plugin_name_required' => 'プラグイン名は必須です。',
'plugin_name_string' => 'プラグイン名は文字列である必要があります。',
'plugin_name_max' => 'プラグイン名は最大 :max 文字まで入力できます。',
'plugin_name_max' => 'プラグイン名は最大 :max 文字までです。',
'name_required' => 'プラグイン名は必須です。',
'name_string' => 'プラグイン名は文字列である必要があります。',
'name_max' => 'プラグイン名は最大 255 文字まで入力できます。',
'search_max' => '検索キーワードは最大 255 文字まで入力できます。',
'name_max' => 'プラグイン名は最大 255 文字までです。',
'search_max' => '検索ワードは最大 255 文字までです。',
'filters_max' => '検索条件は最大 10 個まで設定できます。',
'filter_field_required' => '検索フィールドは必須です。',
'filter_field_invalid' => '無効な検索フィールドです。',
'filter_value_required' => '検索値は必須です。',
'filter_value_max' => '検索値は最大 255 文字まで入力できます。',
'filter_value_max' => '検索値は最大 255 文字までです。',
'filter_operator_invalid' => '無効な検索演算子です。',
'status_invalid' => '無効なステータス値です。',
'per_page_min' => 'ページあたりの項目数は最小 1 個以上である必要があります。',
@@ -130,42 +132,42 @@ return [
'status' => 'ステータス',
],
'status' => [
'active' => '有効',
'inactive' => '無効',
'active' => '有効化',
'inactive' => '無効化',
'uninstalled' => '未インストール',
],
'no_plugins' => 'プラグインがありません。',
'summary' => '合計 :total 個のプラグイン (インストール済み: :installed、有効: :active)',
'summary' => '合計 :total 個のプラグイン (インストール済み: :installed、有効化: :active)',
'invalid_status' => '無効なステータスです。(installed、uninstalled、active、inactive)',
],
'install' => [
'success' => 'プラグイン ":plugin" のインストールが完了しました',
'success' => 'プラグイン ":plugin" インストール完了',
'vendor' => 'ベンダー: :vendor',
'version' => 'バージョン: :version',
'roles_created' => ':count 個のロールが作成されました',
'permissions_created' => ':count 個の権限が作成されました',
'already_installed' => 'プラグイン ":plugin" は既にインストールされています。',
'force_reinstall' => 'プラグイン ":plugin" を強制的に再インストールします (有効ディレクトリを上書き)...',
'force_reinstall' => 'プラグイン ":plugin" を強制再インストールします (有効化ディレクトリを上書き)...',
],
'activate' => [
'success' => 'プラグイン ":plugin" の有効化が完了しました',
'success' => 'プラグイン ":plugin" 有効化完了',
'not_installed' => 'プラグイン ":plugin" はインストールされていません。',
'already_active' => 'プラグイン ":plugin" は既に有効化されています。',
'layouts_registered' => ':count 個のレイアウトが登録されました',
],
'deactivate' => [
'success' => 'プラグイン ":plugin" の無効化が完了しました',
'success' => 'プラグイン ":plugin" 無効化完了',
'not_installed' => 'プラグイン ":plugin" はインストールされていません。',
'not_active' => 'プラグイン ":plugin" は有効化されていません。',
'warning' => 'プラグインのルートと機能が無効化されました。',
'layouts_deleted' => ':count 個のレイアウトが削除されました',
],
'uninstall' => [
'success' => 'プラグイン ":plugin" の削除が完了しました',
'success' => 'プラグイン ":plugin" 削除完了',
'roles_deleted' => ':count 個のロールが削除されました',
'permissions_deleted' => ':count 個の権限が削除されました',
'layouts_deleted' => ':count 個のレイアウトが削除されました',
'confirm_prompt' => 'プラグイン ":plugin" を削除しますか?',
'confirm_prompt' => 'プラグイン ":plugin" を削除してもよろしいですか?',
'confirm_details' => [
'roles' => '- :count 個のロールが削除されます。',
'permissions' => '- :count 個の権限が削除されます。',
@@ -173,18 +175,18 @@ return [
'data' => '- すべてのプラグインデータが削除されます。',
],
'confirm_question' => '本当に削除してもよろしいですか?',
'aborted' => 'プラグインの削除がキャンセルされました。',
'aborted' => 'プラグイン削除がキャンセルされました。',
'not_installed' => 'プラグイン ":plugin" はインストールされていません。',
],
'cache_clear' => [
'clearing_all' => 'すべてのプラグインキャッシュを削除します...',
'clearing_single' => 'プラグイン ":plugin" のキャッシュを削除します...',
'success_all' => 'プラグインキャッシュの削除が完了しました (:count 個の項目)',
'success_single' => 'プラグイン ":plugin" のキャッシュ削除が完了しました (:count 個の項目)',
'clearing_single' => 'プラグイン ":plugin" キャッシュを削除します...',
'success_all' => 'プラグインキャッシュ削除完了 (:count 個の項目)',
'success_single' => 'プラグイン ":plugin" キャッシュ削除完了 (:count 個の項目)',
],
'check_updates' => [
'not_installed' => 'プラグイン ":plugin" はインストールされていません。',
'no_installed' => 'インストール済みのプラグインがありません。',
'no_installed' => 'インストールされたプラグインがありません。',
'update_available' => 'アップデート利用可能',
'up_to_date' => '最新',
'headers' => [
@@ -194,38 +196,36 @@ return [
'source' => 'ソース',
'status' => 'ステータス',
],
'summary' => '合計 :total個を確認しました(アップデート可能: :updates個)',
'single_up_to_date' => 'プラグイン":plugin"は最新バージョンです。(v:version)',
'single_update_available' => 'プラグイン":plugin"がアップデート可能です: v:current → v:latest(:source)',
'summary' => '合計 :total個を確認しました (アップデート可能: :updates個)',
'single_up_to_date' => 'プラグイン ":plugin" は最新バージョンです。(v:version)',
'single_update_available' => 'プラグイン ":plugin" のアップデートが利用可能です: v:current → v:latest (:source)',
],
'update' => [
'success' => 'プラグイン":plugin"のアップデートが完了しました',
'success' => 'プラグイン ":plugin" のアップデートが完了しました',
'version_change' => 'v:from → v:to',
'no_update' => 'プラグイン":plugin"に利用可能なアップデートがありません。',
'not_installed' => 'プラグイン":plugin"はインストールされていません。',
'no_update' => 'プラグイン ":plugin" に利用可能なアップデートはありません。',
'not_installed' => 'プラグイン ":plugin" はインストールされていません。',
'current_version' => '現在のバージョン: :version',
'latest_version' => '最新バージョン: :version',
'update_source' => 'アップデートソース: :source',
'confirm_question' => 'アップデートを実行しますか?',
'confirm_question' => 'アップデートを実行しますか?',
'aborted' => 'アップデートがキャンセルされました。',
'backup_restored' => 'バックアップから前のバージョンが復元されました。',
'force_mode' => '強制アップデートモード: バージョン比較をスキップして再インストールします。',
'layout_strategy' => 'レイアウト戦略: :strategy',
'modified_extensions_warning' => '管理者が編集したレイアウト拡張 :count件が上書きされます。',
'modified_extension_item' => ' - :target (出典: :source)',
'modified_extension_item' => ' - :target (ソース: :source)',
],
],
'composer_install' => [
'start' => 'プラグイン":plugin"のComposer依存関係をインストール中です...',
'success' => 'プラグイン":plugin"のComposer依存関係のインストールが完了しました',
'no_dependencies' => 'プラグイン":plugin"にインストールするComposer依存関係がありません。',
'failed' => 'プラグイン":plugin"のComposer依存関係のインストールに失敗しました。',
'start' => 'プラグイン ":plugin" の Composer 依存関係をインストールしています...',
'success' => 'プラグイン ":plugin" の Composer 依存関係のインストールが完了しました',
'no_dependencies' => 'プラグイン ":plugin" にインストールする Composer 依存関係はありません。',
'failed' => 'プラグイン ":plugin" の Composer 依存関係のインストールに失敗しました。',
'summary' => '📊 結果: 成功 :success個、スキップ :skip個、失敗 :fail個',
],
'settings' => [
'update_failed' => 'プラグイン設定のアップデートに失敗しました。',
'update_failed' => 'プラグイン設定のアップデートに失敗しました: :error',
'updated' => 'プラグイン設定が正常にアップデートされました。',
],
'dependent_templates_success' => '依存テンプレート情報を正常に取得しました。',
'dependent_templates_failed' => '依存テンプレート情報の取得に失敗しました: :error',
];
@@ -14,7 +14,7 @@ return [
'name_required' => 'ロール名は必須です。',
'identifier_required' => '識別子は必須です。',
'identifier_format' => '識別子は小文字で始まり、小文字、数字、アンダースコア(_)のみ使用できます。',
'identifier_unique' => '既に使用中の識別子です。',
'identifier_unique' => 'すでに使用中の識別子です。',
'identifier_max' => '識別子は最大100文字まで入力できます。',
'permission_ids_array' => '権限リストは配列形式である必要があります。',
'permission_ids_exists' => '選択した権限の中に無効な権限があります。',
@@ -1,36 +1,39 @@
<?php
return [
'fetch_success' => 'スケジュール一覧を取得しました。',
'fetch_failed' => 'スケジュール一覧の取得に失敗しました。',
'fetch_success' => 'スケジュールリストを閲覧しました。',
'fetch_failed' => 'スケジュールリスト閲覧に失敗しました。',
'create_success' => 'スケジュールが作成されました。',
'create_failed' => 'スケジュールの作成に失敗しました。',
'create_failed' => 'スケジュール作成に失敗しました。',
'update_success' => 'スケジュールが編集されました。',
'update_failed' => 'スケジュールの編集に失敗しました。',
'update_failed' => 'スケジュール編集に失敗しました。',
'delete_success' => 'スケジュールが削除されました。',
'delete_failed' => 'スケジュールの削除に失敗しました。',
'delete_failed' => 'スケジュール削除に失敗しました。',
'run_success' => 'スケジュールが実行されました。',
'run_failed' => 'スケジュールの実行に失敗しました。',
'run_failed' => 'スケジュール実行に失敗しました。',
'duplicate_success' => 'スケジュールが複製されました。',
'duplicate_failed' => 'スケジュールの複製に失敗しました。',
'duplicate_failed' => 'スケジュール複製に失敗しました。',
'bulk_status_updated' => '選択されたスケジュールのステータスが変更されました。',
'bulk_update_status_failed' => 'スケジュール一括ステータス変更に失敗しました。',
'bulk_update_status_failed' => 'スケジュールステータス一括変更に失敗しました。',
'bulk_delete_success' => '選択されたスケジュールが削除されました。',
'bulk_delete_failed' => 'スケジュール一括削除に失敗しました。',
'statistics_success' => 'スケジュール統計を取得しました。',
'statistics_failed' => 'スケジュール統計の取得に失敗しました。',
'history_fetch_success' => '実行履歴を取得しました。',
'history_fetch_failed' => '実行履歴の取得に失敗しました。',
'statistics_success' => 'スケジュール統計を閲覧しました。',
'statistics_failed' => 'スケジュール統計閲覧に失敗しました。',
'history_fetch_success' => '実行履歴を閲覧しました。',
'history_fetch_failed' => '実行履歴閲覧に失敗しました。',
'history_delete_success' => '実行履歴が削除されました。',
'history_delete_failed' => '実行履歴の削除に失敗しました。',
'history_delete_failed' => '実行履歴削除に失敗しました。',
'history_not_found' => '実行履歴が見つかりません。',
'copy' => 'コピー',
'shell_command_failed' => 'シェルコマンドの実行に失敗しました。',
'http_request_failed' => 'HTTPリクエスト失敗(ステータス::status)',
'shell_command_failed' => 'シェルコマンド実行に失敗しました。',
'http_request_failed' => 'HTTP リクエスト失敗(ステータス::status)',
'url_not_public' => '内部ネットワークアドレス(プライベート IP·localhost など)は呼び出せません。社内アドレスを呼び出す必要がある場合は、環境設定 > セキュリティで「内部ネットワークアドレス呼び出し許可」をオンにしてください。',
'shell_not_allowed' => '許可されていないシェルコマンドです。サーバーに登録された実行ファイルのみスケジュール実行できます。',
'artisan_not_allowed' => 'この Artisan コマンドはセキュリティ上の理由からスケジュール実行できません。',
'type' => [
'artisan' => 'Artisanコマンド',
'artisan' => 'Artisan コマンド',
'shell' => 'シェルコマンド',
'url' => 'URL呼び出し',
'url' => 'URL 呼び出し',
],
'frequency' => [
'everyMinute' => '毎分',
@@ -47,7 +50,7 @@ return [
'never' => '未実行',
],
'trigger_type' => [
'scheduled' => 'スケジュール実行',
'scheduled' => '予約実行',
'manual' => '手動実行',
],
'duration' => [
@@ -56,51 +59,48 @@ return [
],
'validation' => [
'page_integer' => 'ページ番号は整数である必要があります。',
'page_min' => 'ページ番号は1以上である必要があります。',
'page_min' => 'ページ番号は 1 以上である必要があります。',
'per_page_integer' => 'ページあたりの項目数は整数である必要があります。',
'per_page_min' => 'ページあたりの項目数は1以上である必要があります。',
'per_page_max' => 'ページあたりの項目数は100以下である必要があります。',
'per_page_min' => 'ページあたりの項目数は 1 以上である必要があります。',
'per_page_max' => 'ページあたりの項目数は 100 以下である必要があります。',
'type_invalid' => '無効なタスクタイプです。',
'frequency_invalid' => '無効な実行周期です。',
'status_invalid' => '無効なステータスです。',
'last_result_invalid' => '無効な実行結果です。',
'history_status_invalid' => '無効な履歴ステータスです。',
'trigger_type_invalid' => '無効なトリガータイプです。',
'created_from_invalid' => '開始日のフォーマットが正しくありません。',
'created_to_invalid' => '終了日のフォーマットが正しくありません。',
'created_from_invalid' => '開始日の形式が正しくありません。',
'created_to_invalid' => '終了日の形式が正しくありません。',
'created_to_after_from' => '終了日は開始日以降である必要があります。',
'started_from_invalid' => '開始日のフォーマットが正しくありません。',
'started_to_invalid' => '終了日のフォーマットが正しくありません。',
'started_from_invalid' => '開始日の形式が正しくありません。',
'started_to_invalid' => '終了日の形式が正しくありません。',
'started_to_after_from' => '終了日は開始日以降である必要があります。',
'sort_by_invalid' => '無効なソート基準です。',
'sort_order_invalid' => '無効なソート順序です。',
'name_required' => 'タスク名は必須です。',
'name_max' => 'タスク名は255文字以下である必要があります。',
'name_max' => 'タスク名は 255 文字以下である必要があります。',
'type_required' => 'タスクタイプは必須です。',
'command_required' => 'コマンドは必須です。',
'command_max' => 'コマンドは2000文字以下である必要があります。',
'expression_required' => 'Cron式は必須です。',
'expression_max' => 'Cron式は100文字以下である必要があります。',
'command_max' => 'コマンドは 2000 文字以下である必要があります。',
'expression_required' => 'Cron 式は必須です。',
'expression_max' => 'Cron 式は 100 文字以下である必要があります。',
'frequency_required' => '実行周期は必須です。',
'timeout_integer' => 'タイムアウト制限は整数である必要があります。',
'timeout_min' => 'タイムアウト制限は1秒以上である必要があります。',
'timeout_max' => 'タイムアウト制限は86400秒(24時間)以下である必要があります。',
'ids_required' => 'IDリストは必須です。',
'ids_array' => 'IDリストは配列である必要があります。',
'ids_min' => '最低1つ以上のIDが必要です。',
'id_integer' => 'IDは整数である必要があります。',
'id_exists' => '存在しないスケジュールです。',
'is_active_required' => '有効化は必須です。',
'is_active_boolean' => '有効化はtrue/falseである必要があります。',
'timeout_integer' => 'タイムアウトは整数である必要があります。',
'timeout_min' => 'タイムアウトは 1 秒以上である必要があります。',
'timeout_max' => 'タイムアウトは 86400 秒(24 時間)以下である必要があります。',
'ids_required' => 'ID リストは必須です。',
'ids_array' => 'ID リストは配列である必要があります。',
'ids_min' => '最小 1 個以上の ID が必要です。',
'id_integer' => 'ID は整数である必要があります。',
'id_exists' => 'スケジュールが存在しません。',
'is_active_required' => '有効化の有無は必須です。',
'is_active_boolean' => '有効化の有無は真/偽である必要があります。',
],
'permissions' => [
'read' => 'スケジュール調査',
'read' => 'スケジュール閲覧',
'create' => 'スケジュール作成',
'update' => 'スケジュール編集',
'delete' => 'スケジュール削除',
'run' => 'スケジュール実行',
],
'url_not_public' => '内部ネットワークアドレス(プライベートIP·localhost等)は呼び出せません。社内アドレスを呼び出す必要がある場合は、環境設定 > セキュリティで「内部ネットワークアドレス呼び出し許可」をオンにしてください。',
'shell_not_allowed' => '許可されていないシェルコマンドです。サーバーに登録された実行ファイルのみスケジュールで実行できます。',
'artisan_not_allowed' => 'このArtisanコマンドはセキュリティ上の理由によりスケジュールで実行できません。',
];
@@ -6,7 +6,7 @@ return [
'no_results' => '検索結果がありません。',
'view_more' => 'もっと見る',
'validation' => [
'q_min' => '検索語は2文字以上入力してください。',
'q_min' => '検索語は2文字以上で入力してください。',
'q_max' => '検索語は最大200文字まで入力可能です。',
'page_integer' => 'ページ番号は数字である必要があります。',
'page_min' => 'ページ番号は1以上である必要があります。',
@@ -1,21 +1,24 @@
<?php
return [
'cache_cleared_all' => 'SEOキャッシュがすべて削除されました。',
'cache_cleared_layout' => ':layout レイアウトのSEOキャッシュ :count件が削除されました。',
'warmup_started' => 'SEOキャッシュウォーミングを開始します。',
'warmup_started_layout' => ':layout レイアウトのSEOキャッシュウォーミングを開始します。',
'warmup_dispatched' => 'SEOキャッシュウォーミングが開始されました。',
'sitemap_regenerated' => 'サイトマップの再生成が完了しました。',
'sitemap_regenerate_failed' => 'サイトマップの再生成に失敗しました。',
'sitemap_disabled' => 'サイトマップ生成が無効化されています。',
'stats_period' => '最近 :days 日間のSEOキャッシュ統計',
'cache_cleared_all' => 'SEO キャッシュがすべて削除されました。',
'cache_cleared_layout' => ':layout レイアウトの SEO キャッシュ :count 件が削除されました。',
'warmup_started' => 'SEO キャッシュ ウォーミングアップを開始します。',
'warmup_started_layout' => ':layout レイアウトの SEO キャッシュ ウォーミングアップを開始します。',
'warmup_dispatched' => 'SEO キャッシュ ウォーミングアップが開始されました。',
'sitemap_regenerated' => 'Sitemap 再生成が完了しました。',
'sitemap_regenerate_dispatched' => 'Sitemap 再生成を開始しました。完了まで時間がかかる場合があります。',
'sitemap_regenerate_failed' => 'Sitemap 再生成に失敗しました。',
'sitemap_disabled' => 'Sitemap 生成が無効化されています。',
'sitemap_not_ready' => 'Sitemap を準備中です。しばらく後に再度お試しください。',
'sitemap_generate_failed' => 'Sitemap 生成に失敗しました: :error',
'stats_period' => '最近 :days 日間の SEO キャッシュ統計',
'stats_overall' => '全体統計',
'stats_metric' => '項目',
'stats_value' => '値',
'stats_total_entries' => '総キャッシュ項目',
'stats_hits' => 'キャッシュヒット',
'stats_misses' => 'キャッシュミス',
'stats_hits' => 'キャッシュ ヒット',
'stats_misses' => 'キャッシュ ミス',
'stats_hit_rate' => 'ヒット率',
'stats_avg_response_time' => '平均応答時間',
'stats_by_layout' => 'レイアウト別統計',
@@ -5,12 +5,12 @@ return [
'fetch_failed' => '設定のインポートに失敗しました。',
'save_success' => '設定が正常に保存されました。',
'save_failed' => '設定の保存に失敗しました。',
'update_success' => '設定が正常に更新されました。',
'update_failed' => '設定の更新に失敗しました。',
'update_success' => '設定が正常にアップデートされました。',
'update_failed' => '設定のアップデートに失敗しました。',
'delete_success' => '設定が正常に削除されました。',
'delete_failed' => '設定の削除に失敗しました。',
'save_error' => '設定の保存中にエラーが発生しました。',
'update_error' => '設定の更新中にエラーが発生しました。',
'update_error' => '設定のアップデート中にエラーが発生しました。',
'cache_clear_success' => 'キャッシュが正常にクリアされました。',
'cache_clear_failed' => 'キャッシュのクリアに失敗しました。',
'cache_clear_error' => 'キャッシュのクリア中にエラーが発生しました。',
@@ -20,7 +20,11 @@ return [
'backup_success' => 'データベースバックアップが正常に開始されました。',
'backup_failed' => 'データベースバックアップに失敗しました。',
'backup_error' => 'データベースバックアップ中にエラーが発生しました。',
'save_individual_failed' => '設定の保存に失敗しました::error',
'backup_path_required' => 'バックアップパスを入力してください。',
'restore_success' => '設定が正常に復元されました。',
'restore_failed' => '設定の復元に失敗しました。',
'restore_error' => '設定の復元中にエラーが発生しました。',
'save_individual_failed' => '設定の保存に失敗しました: :error',
'invalid_password' => 'パスワードが一致しません。',
'password_required' => 'パスワードを入力してください。',
'app_key_regenerated' => 'アプリケーションキーが正常に再生成されました。',
@@ -59,10 +63,10 @@ return [
'ses' => 'SES (Amazon)',
],
],
'driver_test_success' => 'すべてのドライバー接続テストが成功しました。',
'driver_test_partial' => '一部のドライバー接続テストが失敗しました。',
'driver_test_error' => 'ドライバー接続テスト中にエラーが発生しました。',
'unknown_driver' => '不明なドライバーです。',
'driver_test_success' => 'すべてのドライバ接続テストが成功しました。',
'driver_test_partial' => '一部のドライバ接続テストが失敗しました。',
'driver_test_error' => 'ドライバ接続テスト中にエラーが発生しました。',
'unknown_driver' => '不明なドライバです。',
's3_test_success' => 'S3バケットに正常に接続されました。',
's3_test_failed' => 'S3バケットへの接続に失敗しました。',
's3_missing_config' => 'S3設定が不足しています。(バケット、リージョン、アクセスキー、シークレットキー)',
@@ -82,10 +86,11 @@ return [
'memcached_connection_failed' => 'Memcachedサーバーに接続できません。',
'websocket_test_success' => 'Websocketサーバーに正常に接続されました。',
'websocket_test_failed' => 'Websocketサーバーへの接続に失敗しました。',
'websocket_connection_refused' => 'Websocketサーバーに接続できません。サーバーが実行されているか確認してください。',
'websocket_invalid_host' => 'Websocketホスト設定が正しくありません。ホストにはアドレスのみを入力し、アカウント情報(@)またはhttp/https以外のプロトコルは使用できません。',
'websocket_connection_refused' => 'Websocketサーバーに接続できません。サーバーが実行中であるか確認してください。',
'invalid_email' => '無効なメールアドレスです。',
'test_mail_subject' => ':app_name テストメール',
'test_mail_body' => 'これはグヌボード7から送信されたテストメールです。このメールを受け取った場合、メール設定が正しく構成されています。',
'test_mail_body' => 'これはGnuboard 7から送信されたテストメールです。このメールを受け取られた場合、メール設定が正しく構成されています。',
'test_mail_sent' => 'テストメールが正常に送信されました。',
'test_mail_failed' => 'テストメールの送信に失敗しました。',
'test_mail_error' => 'テストメール送信中にエラーが発生しました。',
@@ -93,65 +98,60 @@ return [
'check_success' => 'アップデート確認が完了しました。',
'check_failed' => 'アップデート確認に失敗しました。',
'update_available' => '新しいアップデートがあります。',
'no_update' => '現在は最新バージョンです。',
'maintenance_mode_active' => 'システム保守中です。しばらく後に再度お試しください。',
'no_update' => '現在、最新バージョンです。',
'maintenance_mode_active' => 'システムメンテナンス中です。しばらく後に再度お試しください。',
'invalid_github_url' => 'GitHubリポジトリURLが無効です。',
'download_failed' => ':version バージョンのダウンロードに失敗しました。',
'zip_extract_failed' => 'ZIPファイルの解凍に失敗しました。',
'zip_extract_failed' => 'ZIPファイルの展開に失敗しました。',
'invalid_package' => 'ダウンロードされたパッケージが無効です。',
'invalid_package_not_g7' => '指定されたディレクトリはグヌボード7プロジェクトではありません。config/app.phpファイルとversionの設定が必要です。',
'composer_failed' => 'composer installの実行に失敗しました。',
'pending_path_create_failed' => '_pendingディレクトリ(:path)の作成に失敗しました::error',
'pending_path_not_writable' => '_pendingディレクトリ(:path)への書き込み権限がありません。',
'invalid_package_not_g7' => '指定されたディレクトリはGnuboard 7プロジェクトではありません。config/app.phpファイルとversion設定が必要です。',
'composer_failed' => 'composer install の実行に失敗しました。',
'pending_path_create_failed' => '_pending ディレクトリ(:path) の作成に失敗しました: :error',
'pending_path_not_writable' => '_pending ディレクトリ(:path) への書き込み権限がありません。',
'downloading' => 'アップデートをダウンロード中...',
'extracting' => '解凍中...',
'validating' => 'パッケージを検証中...',
'running_composer' => 'composer installを実行中...',
'running_composer' => 'composer install を実行中...',
'step_check' => 'アップデートを確認中...',
'step_validate_pending' => '環境を検証中...',
'step_maintenance' => 'メンテナンスモードを有効化中...',
'step_download' => 'ダウンロード中...',
'step_backup' => 'バックアップを作成中...',
'step_apply' => 'ファイルを適用中...',
'step_composer' => 'composer installを実行中...',
'step_migration' => 'マイグレーションを実行中...',
'step_composer' => 'composer install 中...',
'step_migration' => 'マイグレーション を実行中...',
'step_upgrade' => 'アップグレードステップを実行中...',
'step_composer_prod' => '運用ディレクトリのcomposer installを実行中...',
'step_composer_prod' => '運用ディレクトリの composer install 中...',
'step_cleanup' => 'クリーンアップ中...',
'github_url_not_configured' => 'GitHubリポジトリのURLが設定されていません。',
'github_api_failed' => 'GitHub APIに接続できません。',
'github_token_required' => 'プライベートリポジトリです。GitHubアクセストークンを設定してください。',
'github_token_invalid' => 'GitHubアクセストークンが無効であるか、権限が不足しています。(HTTP :status — :message)',
'github_repo_not_found' => 'GitHubリポジトリが見つかりません。(HTTP :status — :message)',
'github_repo_not_found_no_token' => 'GitHubリポジトリが見つかりません。プライベートリポジトリの場合はアクセストークンを設定してください。(HTTP :status — :message)',
'github_api_error' => 'GitHub APIエラーが発生しました。(HTTP :status — :message)',
'no_releases_found' => 'GitHubリポジトリにリリースがありません。(HTTP :status — :message)',
'log_api_call_failed' => 'コアアップデート:GitHub APIの呼び出しに失敗しました',
'log_auth_failed' => 'コアアップデート:GitHub認証に失敗しました',
'log_not_found' => 'コアアップデート:GitHubリポジトリ/リリースが見つかりません',
'log_unexpected_status' => 'コアアップデート:予期しないHTTPステータスコード',
'log_version_check_error' => 'コアアップデート:最新バージョン確認中にエラーが発生しました',
'unknown_error' => '予期しないエラーが発生しました。',
'apply_mode_incremental' => 'ファイル適用: コアが実際に変更したファイルのみを適用しました (新規 :added 個、変更 :changed 個)。その他のファイルは現在の状態のまま保持されました。',
'apply_mode_incremental_prune_hint' => '新バージョンで削除されたファイルをあわせてクリーンアップするには、`--prune` オプションで再実行するか、`php artisan hotfix:rollback-stale-files --prune` を使用してください。',
'apply_mode_prune' => 'ファイル適用: 全体上書き + 削除ファイルのクリーンアップを実行しました (--prune)。',
'apply_mode_fallback' => 'ファイル適用: バックアップがないため、増分適用をスキップして全体上書きを実行しました。ユーザーのカスタムファイル保持をご希望の場合は、次回以降はバックアップを保持したままアップデートしてください。',
'github_url_not_configured' => 'GitHub リポジトリ URL が設定されていません。',
'github_api_failed' => 'GitHub API に接続できません。',
'github_token_required' => 'プライベートリポジトリです。GitHub アクセストークンを設定してください。',
'github_token_invalid' => 'GitHub アクセストークンが無効であるか、権限が不足しています。(HTTP :status — :message)',
'github_repo_not_found' => 'GitHub リポジトリが見つかりません。(HTTP :status — :message)',
'github_repo_not_found_no_token' => 'GitHub リポジトリが見つかりません。プライベートリポジトリの場合はアクセストークンを設定してください。(HTTP :status — :message)',
'github_api_error' => 'GitHub API エラーが発生しました。(HTTP :status — :message)',
'no_releases_found' => 'GitHub リポジトリにリリースがありません。(HTTP :status — :message)',
'log_api_call_failed' => 'コアアップデート: GitHub API 呼び出し失敗',
'log_auth_failed' => 'コアアップデート: GitHub 認証失敗',
'log_not_found' => 'コアアップデート: GitHub リポジトリ/リリース未検出',
'log_unexpected_status' => 'コアアップデート: 予期しない HTTP ステータスコード',
'log_version_check_error' => 'コアアップデート: 最新バージョン確認時にエラーが発生',
'unknown_error' => '不明なエラーが発生しました。',
'system_requirements_failed' => 'システム要件が満たされていません:',
'no_extract_method_available' => '利用可能なアーカイブ抽出方法がありません。PHP zip拡張機能(ZipArchive)またはunzipコマンド(Linux)のいずれかが必要です。または--sourceオプションで手動アップデートできます。',
'manual_update_guide' => '手動アップデート:GitHubからリリースZIPをダウンロードして解凍し、php artisan core:update --source=/解凍パス コマンドを実行してください。',
'archive_url_not_found' => ':typeアーカイブURLが見つかりません。次の方法を試みます。',
'extracting_with' => ':method方式で解凍中...',
'no_extract_method_available' => '利用可能なアーカイブ抽出方法がありません。PHP zip 拡張 (ZipArchive) または unzip コマンド (Linux) のいずれかが必要です。または --source オプションで手動アップデートできます。',
'manual_update_guide' => '手動アップデート: GitHub からリリース ZIP をダウンロードして解凍後、php artisan core:update --source=/解凍/パス コマンドを実行してください。',
'archive_url_not_found' => ':type アーカイブ URL が見つかりません。次の方法を試します。',
'extracting_with' => ':method 方式で解凍中...',
'extract_empty' => '解凍されたディレクトリが空です。',
'extract_fallback' => ':method方式に失敗しました::error — 次の方法を試みます。',
'all_extract_methods_failed' => 'すべてのアーカイブ抽出方法が失敗しました。--sourceオプションで手動アップデートを試みてください。',
'unzip_command_failed' => 'unzipコマンド実行に失敗しました(終了コード::code)。:output',
'zip_file_not_found' => '指定されたZIPファイルが見つかりません::path',
'source_vendor_missing' => 'ソースディレクトリにvendorがありません。composer installが実行されていない可能性があります。',
'composer_failed_with_output' => 'composer installの実行に失敗しました。:output',
'apply_mode_incremental' => 'ファイル適用: コアが実際に変更したファイルのみ適用しました (新規 :added個、変更 :changed個)。その他のファイルは現在の状態のまま保存されました。',
'apply_mode_incremental_prune_hint' => '新しいバージョンで削除されたファイルを一緒に整理する場合は、`--prune` オプションで再実行するか、`php artisan hotfix:rollback-stale-files --prune` を使用してください。',
'apply_mode_prune' => 'ファイル適用: 完全な上書き + 削除されたファイルの整理を実行しました (--prune)。',
'apply_mode_fallback' => 'ファイル適用: バックアップがないため増分適用をスキップし、完全な上書きを実行しました。ユーザーカスタムファイルの保存を希望する場合は、次回からはバックアップを維持したまま更新してください。',
'extract_fallback' => ':method 方式失敗: :error — 次の方法を試します。',
'all_extract_methods_failed' => 'すべてのアーカイブ抽出方法が失敗しました。--source オプションで手動アップデートを試してください。',
'unzip_command_failed' => 'unzip コマンド実行失敗 (終了コード: :code)。:output',
'zip_file_not_found' => '指定された ZIP ファイルが見つかりません: :path',
'source_vendor_missing' => 'ソースディレクトリに vendor がありません。composer install が実行されていない可能性があります。',
'composer_failed_with_output' => 'composer install 実行に失敗しました。:output',
],
'backup_path_required' => 'バックアップパスを入力してください。',
'restore_success' => '設定が正常に復元されました。',
'restore_failed' => '設定の復元に失敗しました。',
'restore_error' => '設定の復元中にエラーが発生しました。',
'websocket_invalid_host' => 'Websocketホスト設定が正しくありません。ホストにはアドレスのみを入力し、アカウント情報(@)やhttp/https以外のプロトコルは使用できません。',
];
@@ -4,54 +4,54 @@ return [
'title' => 'テンプレート管理',
'errors' => [
'force_update_no_source' => ':template を強制アップデートするソースが見つかりません。バンドルおよび GitHub URL の両方がありません。',
'downgrade_blocked' => 'ダウングレードがブロックされました (:from → :to)。意図的なダウングレードは --force オプションを使用してください。',
'downgrade_blocked' => 'ダウングレードがブロックされています (:from → :to)。意図的なダウングレードは --force オプションを使用してください。',
'not_found' => 'テンプレートが見つかりません。(:template)',
'not_installed' => 'テンプレートがインストールされていません。(:template)',
'already_active' => '既に有効化されているテンプレートです。',
'invalid_format' => '無効なテンプレート形式です。',
'activation_failed' => 'テンプレート有効化に失敗しました。',
'file_copy_failed' => 'ファイルコピーに失敗しました。',
'dist_directory_not_found' => 'テンプレートビルド結果物(dist)が見つかりません。',
'activation_failed' => 'テンプレートの有効化に失敗しました: :error',
'file_copy_failed' => 'ファイルのコピーに失敗しました: :error',
'dist_directory_not_found' => 'テンプレートビルド成果物(dist)が見つかりません。',
'invalid_directory_name' => '無効なディレクトリ名です。vendor-name 形式を使用してください。',
'dependency_not_met' => '必須の依存関係 :dependency (:type)が満たされていません。',
'dependency_not_met' => '必須依存関係 :dependency (:type) が満たされていません。',
'version_mismatch' => ':dependency のバージョンが一致しません。(必須: :required、インストール済み: :installed)',
'deactivation_failed' => 'テンプレート無効化に失敗しました。',
'installation_failed' => 'テンプレートインストールに失敗しました。',
'uninstallation_failed' => 'テンプレート削除に失敗しました。',
'deactivation_failed' => 'テンプレートの無効化に失敗しました: :error',
'installation_failed' => 'テンプレートのインストールに失敗しました: :error',
'uninstallation_failed' => 'テンプレートの削除に失敗しました: :error',
'template_json_not_found' => 'template.json ファイルが見つかりません。',
'template_json_invalid' => 'template.json ファイルが無効です。',
'missing_required_fields' => '必須フィールドが不足しています。',
'invalid_type' => '無効なテンプレートタイプです。(admin または user のみ許可)',
'copy_rollback_failed' => 'ファイルコピーロールバックに失敗しました。',
'copy_rollback_failed' => 'ファイルコピーのロールバックに失敗しました: :error',
'invalid_layout_json' => 'レイアウト JSON ファイルが無効です。',
'missing_layout_name' => 'レイアウト JSON に layout_name フィールドが不足しています。',
'layout_registration_failed' => 'レイアウト登録に失敗しました。',
'layout_validation_failed' => ':template テンプレートのレイアウト検証に失敗しました。(:count 個のエラー)',
'layout_registration_failed' => 'レイアウト登録に失敗しました: :error',
'layout_validation_failed' => ':template テンプレートのレイアウト検証に失敗しました。(:count 件のエラー)',
'layout_validation_json_error' => ':file - JSON パース エラー: :error',
'layout_validation_missing_name' => ':file - layout_name フィールドが不足しています。',
'layout_validation_partial_error' => ':file - Partial 処理エラー (:partial): :error',
'override_layout_registration_failed' => 'オーバーライドレイアウト登録に失敗しました。',
'override_layout_registration_failed' => 'オーバーライドレイアウト登録に失敗しました: :error',
'file_not_found' => 'ファイルが見つかりません。',
'file_type_not_allowed' => '許可されていないファイルタイプです。',
'components_not_found' => 'コンポーネント定義ファイルが見つかりません。',
'locale_not_supported' => ':template テンプレートは :locale 言語をサポートしていません。',
'language_file_not_found' => ':locale 言語ファイルが見つかりません。',
'invalid_json' => '無効な JSON 形式です。',
'invalid_cache_data' => 'キャッシュデータが破損しました。もう一度お試しください。',
'invalid_cache_data' => 'キャッシュデータが破損しています。もう一度お試しください。',
'unknown_error' => '不明なエラーが発生しました。',
'zip_open_failed' => 'ZIP ファイルを開くことができません。',
'zip_open_failed' => 'ZIP ファイルを開くことができません: :error',
'identifier_missing' => 'テンプレート識別子が不足しています。',
'already_installed' => '既にインストールされているテンプレートです。',
'install_failed' => 'テンプレートインストールに失敗しました。',
'already_installed' => '既にインストール済みのテンプレートです。',
'install_failed' => 'テンプレートのインストールに失敗しました: :error',
'github_url_invalid' => '無効な GitHub URL 形式です。',
'github_download_failed' => 'GitHub からのダウンロードに失敗しました。',
'github_download_failed' => 'GitHub からのダウンロードに失敗しました: :error',
'github_repo_not_found' => 'GitHub リポジトリが見つかりません。',
'refresh_layouts_failed' => 'レイアウト更新に失敗しました。',
'refresh_layouts_failed' => 'レイアウト更新に失敗しました: :error',
'routes_not_found' => 'ルートファイルが見つかりません。',
'no_active_template' => '有効化されているテンプレートがありません',
'layout_load_failed' => 'レイアウトの読み込みに失敗しました',
'component_load_failed' => 'コンポーネントの読み込みに失敗しました',
'translation_load_failed' => '多言語ファイルの読み込みに失敗しました',
'no_active_template' => '有効化されたテンプレートがありません',
'layout_load_failed' => 'レイアウトの読み込みに失敗しました: :error',
'component_load_failed' => 'コンポーネントの読み込みに失敗しました: :error',
'translation_load_failed' => '多言語ファイルの読み込みに失敗しました: :error',
'template_init_error' => 'テンプレートエンジンの初期化中にエラーが発生しました',
'contact_admin_for_template' => 'システム管理者に問い合わせてテンプレートを有効化してください。',
'missing_error_config' => 'テンプレートに error_config セクションがありません。エラーページレイアウト設定が必要です。',
@@ -61,17 +61,17 @@ return [
'template_not_active' => 'テンプレートが有効化されていません: :identifier',
'dependencies_not_met' => 'テンプレートの必須依存関係が満たされていません。',
'service_unavailable' => 'サービスは一時的に利用できません。',
'contact_admin' => '管理者にお問い合わせください。',
'operation_in_progress' => 'テンプレート「:name」は現在 :status 状態です。処理が完了するまでお待ちください。',
'github_api_failed' => 'GitHub API 呼び出しに失敗しました。',
'contact_admin' => '管理者に問い合わせてください。',
'operation_in_progress' => 'テンプレート ":name" は現在 :status 状態です。処理が完了するまでお待ちください。',
'github_api_failed' => 'GitHub API 呼び出しに失敗しました: :error',
'invalid_github_url' => '無効な GitHub URL です。',
'zip_url_not_found' => 'ZIP ダウンロード URL が見つかりません。',
'download_failed' => 'テンプレート「:template」:version バージョンのダウンロードに失敗しました。',
'zip_extract_failed' => 'ZIP 解凍に失敗しました。',
'extracted_dir_not_found' => '解凍されたディレクトリが見つかりません。',
'reload_failed' => 'テンプレート再読み込みに失敗しました。',
'delete_directory_failed' => 'テンプレートディレクトリの削除に失敗しました。',
'update_failed' => 'テンプレート「:template」のアップデートに失敗しました: :error',
'download_failed' => 'テンプレート ":template" :version バージョンのダウンロードに失敗しました: :error',
'zip_extract_failed' => 'ZIP 圧縮解除に失敗しました: :error',
'extracted_dir_not_found' => '圧縮解除されたディレクトリが見つかりません。',
'reload_failed' => 'テンプレート再読み込みに失敗しました: :error',
'delete_directory_failed' => 'テンプレートディレクトリの削除に失敗しました: :error',
'update_failed' => 'テンプレート ":template" のアップデートに失敗しました: :error',
'invalid_layout_strategy' => '無効なレイアウト戦略です。(overwrite または keep のみ許可)',
],
'messages' => [
@@ -79,43 +79,43 @@ return [
'deactivated' => 'テンプレートが無効化されました。',
'installed' => 'テンプレートがインストールされました。',
'uninstalled' => 'テンプレートが削除されました。',
'previous_template_deactivated' => '前のアクティブテンプレートが無効化されました。',
'previous_template_deactivated' => '前の有効なテンプレートが無効化されました。',
'files_copied' => 'テンプレートファイルがコピーされました。',
'files_removed' => 'テンプレートファイルが削除されました。',
'routes_retrieved' => 'テンプレートルート情報を取得しました。',
'routes_retrieved' => 'テンプレートルート情報が照会されました。',
'routes_not_found' => 'テンプレートルートが見つかりません。',
'layout_served' => 'レイアウトが提供されました。',
'config_retrieved' => 'テンプレート設定情報を照会しました。',
'config_retrieved' => 'テンプレート設定情報を閲覧しました。',
'template_activated' => 'テンプレートが有効化されました。',
'template_deactivated' => 'テンプレートが無効化されました。',
'template_uninstalled' => 'テンプレートが削除されました。',
'editor_spec_empty' => 'エディタースペックが空です。',
'editor_assets_retrieved' => 'エディターアセット情報を取得しました。',
'editor_assets_missing' => 'エディターアセットマニフェストがありません(ビルドされていません)。',
'language_retrieved' => '多言語データを取得しました。',
'editor_spec_retrieved' => 'エディタースペックを閲覧しました。',
'editor_assets_retrieved' => 'エディター資産情報を閲覧しました。',
'editor_assets_missing' => 'エディター資産マニフェストがありません(ビルドされていません)。',
'language_retrieved' => '多言語データを閲覧しました。',
'language_empty' => '多言語データが空です。',
'editor_spec_retrieved' => 'エディタースペックを取得しました。',
],
'not_found' => 'テンプレート :template が見つかりません。',
'layout_not_found' => 'レイアウトが見つかりません。',
'not_found' => 'テンプレート :template を見つけることができません。',
'layout_not_found' => 'レイアウトを見つけることができません。',
'info' => [
'scanning' => 'テンプレートディレクトリをスキャン中です。',
'loading' => 'テンプレートを読み込み中です。',
'validating_dependencies' => '依存関係を検証中です。',
'copying_files' => 'ファイルをコピー中です。',
'removing_files' => 'ファイルを削除中です。',
'template_found' => 'テンプレートが見つかりました。',
'no_templates_found' => 'テンプレートが見つかりませんでした。',
'public_directory_cleaned' => 'publicディレクトリが整理されました。',
'scanning' => 'テンプレートディレクトリをスキャン中...',
'loading' => 'テンプレートをロード中...',
'validating_dependencies' => '依存性を検証中...',
'copying_files' => 'ファイルをコピー中...',
'removing_files' => 'ファイルを削除中...',
'template_found' => 'テンプレートを発見しました。',
'no_templates_found' => 'テンプレートを発見できませんでした。',
'public_directory_cleaned' => 'public ディレクトリがクリーンアップされました。',
'no_layouts_directory' => 'レイアウトディレクトリが存在しません。',
'no_layout_files' => 'レイアウトJSONファイルが存在しません。',
'no_layout_files' => 'レイアウト JSON ファイルが存在しません。',
'layout_registered' => 'レイアウトが登録されました。',
'layouts_deleted' => 'レイアウトが削除されました。',
'cache_cleared' => 'テンプレートキャッシュが削除されました。',
'cache_warmed' => 'テンプレートキャッシュが作成されました。',
'no_overrides_directory' => 'オーバーライドディレクトリが存在しません。',
'no_override_modules' => 'オーバーライド対象のモジュールディレクトリが存在しません。',
'no_override_layouts_for_module' => 'このモジュール用のオーバーライドレイアウトがありません。',
'no_override_layouts_for_module' => '該当モジュールにオーバーライドレイアウトがありません。',
'override_layout_registered' => 'オーバーライドレイアウトが登録されました。',
'override_layouts_registered' => 'オーバーライドレイアウトが登録されました。',
'override_layouts_cache_invalidated' => 'オーバーライドレイアウトキャッシュが無効化されました。',
@@ -129,17 +129,17 @@ return [
'updating' => 'アップデート中',
],
'types' => [
'admin' => '管理者用',
'user' => 'ユーザー用',
'admin' => '管理者向け',
'user' => 'ユーザー向け',
],
'dependencies' => [
'section_title' => '依存関係',
'section_title' => '依存性',
'modules' => '必須モジュール',
'plugins' => '必須プラグイン',
'status_met' => '充足',
'status_unmet' => '未充足',
'no_dependencies' => '依存関係なし',
'version_required' => '必要バージョン: :version',
'status_met' => '満たしている',
'status_unmet' => '満たしていない',
'no_dependencies' => '依存性なし',
'version_required' => '必要なバージョン: :version',
'version_installed' => 'インストール済みバージョン: :version',
'not_installed' => '未インストール',
'not_active' => '無効',
@@ -150,111 +150,111 @@ return [
'name_max' => 'テンプレート名は255文字を超えることはできません。',
'file_required' => 'ファイルは必須です。',
'file_invalid' => '有効なファイルではありません。',
'file_must_be_zip' => 'ZIPファイルのみアップロード可能です。',
'file_must_be_zip' => 'ZIP ファイルのみアップロード可能です。',
'file_max_size' => 'ファイルサイズは :sizeMB を超えることはできません。',
'github_url_required' => 'GitHub URLは必須です。',
'github_url_invalid' => 'GitHub URLが正しくありません。',
'github_url_format' => 'GitHub URLの形式が正しくありません。(例: https://github.com/owner/repo)',
'type_in' => 'テンプレートタイプはユーザーまたは管理者のみ許可されます。',
'github_url_required' => 'GitHub URL は必須です。',
'github_url_invalid' => 'GitHub URL が正しくありません。',
'github_url_format' => 'GitHub URL の形式が正しくありません。(例:https://github.com/owner/repo)',
'type_in' => 'テンプレートタイプは user または admin のみ許可されます。',
'search_max' => '検索キーワードは255文字を超えることはできません。',
'filters_max' => '検索フィルターは最大10個まで許可されます。',
'filter_field_required' => 'フィルターフィールドは必須です。',
'filter_field_invalid' => '許可されていないフィルターフィールドです。',
'filter_field_invalid' => '許可されないフィルターフィールドです。',
'filter_value_required' => 'フィルター値は必須です。',
'filter_value_max' => 'フィルター値は255文字を超えることはできません。',
'filter_operator_invalid' => '許可されていないフィルター演算子です。(like、eq、starts_with、ends_withのみ許可)',
'status_invalid' => '許可されていないステータス値です。(installed、not_installed、active、inactiveのみ許可)',
'per_page_min' => 'ページあたりの項目数は最小1以上である必要があります。',
'per_page_max' => 'ページあたりの項目数は最大100個まで許可されます。',
'filter_operator_invalid' => '許可されないフィルター演算子です。(like、eq、starts_with、ends_with のみ許可)',
'status_invalid' => '許可されない状態値です。(installed、not_installed、active、inactive のみ許可)',
'per_page_min' => 'ページ当たりのアイテム数は最小1つ以上である必要があります。',
'per_page_max' => 'ページ当たりのアイテム数は最大100個まで許可されます。',
'page_min' => 'ページ番号は最小1以上である必要があります。',
],
'install_success' => 'テンプレートが正常にインストールされました。',
'install_failed' => 'テンプレートのインストールに失敗しました。',
'preview_success' => 'manifestプレビューが完了しました。',
'preview_failed' => 'manifestプレビューに失敗しました。',
'install_failed' => 'テンプレートのインストールに失敗しました: :error',
'preview_success' => 'manifest プレビューが完了しました。',
'preview_failed' => 'manifest プレビューに失敗しました: :error',
'uninstall_success' => 'テンプレートが正常に削除されました。',
'uninstall_failed' => 'テンプレートの削除に失敗しました。',
'uninstall_failed' => 'テンプレートの削除に失敗しました: :error',
'activate_success' => 'テンプレートが有効化されました。',
'activate_failed' => 'テンプレートの有効化に失敗しました。',
'activate_warning' => 'テンプレートの有効化に必要な依存関係が充足されていません。',
'activate_failed' => 'テンプレートの有効化に失敗しました: :error',
'activate_warning' => 'テンプレートの有効化に必要な依存関係が満たされていません。',
'warnings' => [
'missing_dependencies' => 'このテンプレートを有効化するには、以下の依存関係が必要です。',
],
'deactivate_success' => 'テンプレートが無効化されました。',
'deactivate_failed' => 'テンプレートの無効化に失敗しました。',
'deactivate_failed' => 'テンプレートの無効化に失敗しました: :error',
'fetch_success' => 'テンプレート情報を正常に取得しました。',
'license_not_found' => 'テンプレートライセンスファイルが見つかりません。',
'refresh_layouts_success' => 'テンプレートレイアウトが正常に更新されました。',
'refresh_layouts_failed' => 'テンプレートレイアウトの更新に失敗しました。',
'update_success' => 'テンプレート「:template」が :version バージョンにアップデートされました。',
'refresh_layouts_failed' => 'テンプレートレイアウトの更新に失敗しました: :error',
'update_success' => 'テンプレート「:template」がバージョン:versionにアップデートされました。',
'update_failed' => 'テンプレートのアップデートに失敗しました: :error',
'update_hook_failed' => 'テンプレートアップデートフックの実行に失敗しました。',
'update_hook_failed' => 'テンプレートアップデートフックの実行に失敗しました: :error',
'no_update_available' => 'アップデートする内容がありません。',
'check_updates_success' => 'アップデート確認が完了しました。',
'check_updates_failed' => 'アップデート確認に失敗しました。',
'not_installed' => 'テンプレート「:template」がインストールされていません。',
'check_modified_layouts_success' => '編集済みレイアウトの確認が完了しました。',
'check_modified_layouts_failed' => '編集済みレイアウトの確認に失敗しました。',
'pending_not_found' => 'インストール待機リストからテンプレート「:template」が見つかりません。',
'check_updates_failed' => 'アップデート確認に失敗しました: :error',
'not_installed' => 'テンプレート「:template」はインストールされていません。',
'check_modified_layouts_success' => '変更されたレイアウト確認が完了しました。',
'check_modified_layouts_failed' => '変更されたレイアウト確認に失敗しました: :error',
'pending_not_found' => 'インストール待機リストにテンプレート「:template」が見つかりません。',
'already_exists' => 'テンプレート「:template」は既に存在します。',
'move_failed' => 'テンプレート「:template」の移動に失敗しました。',
'move_failed' => 'テンプレート「:template」の移動に失敗しました: :error',
'commands' => [
'install' => [
'success' => 'テンプレート「:template」のインストール完了',
'already_installed' => 'テンプレート「:template」は既にインストールされています。',
'force_reinstall' => 'テンプレート「:template」を強制再インストールします(アクティブディレクトリを上書き)...',
'layouts_created' => 'レイアウト :count 個が作成されました',
'force_reinstall' => 'テンプレート「:template」を強制再インストールしています(アクティブディレクトリを上書き)...',
'layouts_created' => 'レイアウト:count個を作成しました',
'type' => 'タイプ: :type',
'version' => 'バージョン: :version',
'directory_not_found' => 'テンプレートディレクトリが見つかりません: :path',
'config_not_found' => 'template.json ファイルが見つかりません',
'config_not_found' => 'template.jsonファイルが見つかりません',
],
'activate' => [
'success' => 'テンプレート「:template」の有効化完了',
'not_installed' => 'テンプレート「:template」がインストールされていません。',
'deactivated_previous' => '既存の :type テンプレート「:previous」を無効化',
'not_installed' => 'テンプレート「:template」はインストールされていません。',
'deactivated_previous' => '既存の:typeテンプレート「:previous」を無効化',
],
'deactivate' => [
'success' => 'テンプレート「:template」の無効化完了',
'not_active' => 'テンプレート「:template」が有効化されていません。',
'no_active_warning' => '現在有効化されている :type テンプレートがありません。',
'not_active' => 'テンプレート「:template」は有効化されていません。',
'no_active_warning' => '現在有効化されている:typeテンプレートがありません。',
],
'uninstall' => [
'success' => 'テンプレート「:template」の削除完了',
'confirm_prompt' => 'テンプレート「:template」を削除してもよろしいですか?',
'confirm_prompt' => 'テンプレート「:template」を削除しますか?',
'confirm_details' => [
'layouts' => '- レイアウト :count 個が削除されます',
'versions' => '- user タイプテンプレートの場合はバージョン履歴も削除されます',
'layouts' => '- レイアウト:count個が削除されます',
'versions' => '- userタイプテンプレートの場合、バージョン履歴も削除されます',
],
'confirm_question' => '続けてもよろしいですか?',
'layouts_deleted' => 'レイアウト :count 個が削除されました',
'versions_deleted' => 'バージョン履歴 :count 個が削除されました',
'confirm_question' => '続行しますか?',
'layouts_deleted' => 'レイアウト:count個を削除しました',
'versions_deleted' => 'バージョン履歴:count個を削除しました',
'aborted' => '削除がキャンセルされました。',
],
'list' => [
'no_templates' => '登録されたテンプレートがありません。',
'invalid_type' => '無効なタイプです。admin または user のみ使用可能です。',
'invalid_status' => '無効なステータスです。installed、uninstalled、active、inactive のみ使用可能です。',
'invalid_type' => '無効なタイプです。adminまたはuserのみ使用できます。',
'invalid_status' => '無効な状態です。installed、uninstalled、active、inactiveのみ使用できます。',
'headers' => [
'identifier' => '識別子',
'name' => '名前',
'type' => 'タイプ',
'version' => 'バージョン',
'status' => 'ステータス',
'status' => '状態',
],
'status' => [
'uninstalled' => '未インストール',
],
'summary' => '合計 :total 個のテンプレート(インストール済み: :installed 個、有効: :active 個)',
'summary' => '合計:total個のテンプレート(インストール済み: :installed個、有効: :active個)',
],
'cache_clear' => [
'clearing_single' => 'テンプレート「:template」のキャッシュを削除中...',
'clearing_all' => 'すべてのテンプレートキャッシュを削除中...',
'success_single' => 'テンプレート「:template」のキャッシュ :count 個の削除完了',
'success_all' => '全テンプレートキャッシュ :count 個の削除完了',
'success_single' => 'テンプレート「:template」のキャッシュ:count個を削除しました',
'success_all' => 'テンプレート全体のキャッシュ:count個を削除しました',
],
'check_updates' => [
'not_installed' => 'テンプレート「:template」がインストールされていません。',
'not_installed' => 'テンプレート「:template」はインストールされていません。',
'no_installed' => 'インストールされたテンプレートがありません。',
'update_available' => 'アップデート可能',
'up_to_date' => '最新',
@@ -263,27 +263,27 @@ return [
'current_version' => '現在のバージョン',
'latest_version' => '最新バージョン',
'source' => 'ソース',
'status' => 'ステータス',
'status' => '状態',
],
'summary' => '合計 :total 個を確認(アップデート可能: :updates 個)',
'summary' => '合計:total個を確認(アップデート可能: :updates個)',
'single_up_to_date' => 'テンプレート「:template」は最新バージョンです。(v:version)',
'single_update_available' => 'テンプレート「:template」がアップデート可能です: v:current → v:latest(:source)',
'single_update_available' => 'テンプレート「:template」アップデート可能: v:current → v:latest(:source)',
],
'update' => [
'success' => 'テンプレート「:template」のアップデート完了',
'version_change' => 'v:from → v:to',
'no_update' => 'テンプレート「:template」に利用可能なアップデートがありません。',
'not_installed' => 'テンプレート「:template」がインストールされていません。',
'invalid_strategy' => '無効なレイアウト戦略です。overwrite または keep のみ使用可能です。',
'not_installed' => 'テンプレート「:template」はインストールされていません。',
'invalid_strategy' => '無効なレイアウト戦略です。overwriteまたはkeepのみ使用できます。',
'current_version' => '現在のバージョン: :version',
'latest_version' => '最新バージョン: :version',
'update_source' => 'アップデートソース: :source',
'layout_strategy' => 'レイアウト戦略: :strategy',
'modified_layouts_warning' => '編集済みレイアウト :count 個が上書きされます:',
'modified_layout_item' => ' - :name(編集日: :date)',
'confirm_question' => 'アップデートを進めてもよろしいですか?',
'modified_layouts_warning' => '変更されたレイアウト:count個が上書きされます:',
'modified_layout_item' => ' - :name(変更日: :date)',
'confirm_question' => 'アップデートを実行しますか?',
'aborted' => 'アップデートがキャンセルされました。',
'backup_restored' => 'バックアップから前のバージョンが復元されました。',
'backup_restored' => 'バックアップから以前のバージョンが復元されました。',
'force_mode' => '強制アップデートモード: バージョン比較をスキップして再インストールします。',
],
],
@@ -303,8 +303,8 @@ return [
'file_invalid' => '正しいファイルではありません。',
'file_image' => '画像ファイルのみアップロードできます。',
'file_mimes' => 'サポートされている画像形式ではありません (jpg, jpeg, png, gif, webp, svg)。',
'file_max' => 'ファイルサイズは最大 :max MBまで許可されています。',
'layout_name_max' => 'レイアウト名は最大150文字まで許可されています。',
'file_max' => 'ファイルサイズは最大 :max MB までです。',
'layout_name_max' => 'レイアウト名は最大 150 文字までです。',
],
],
];
@@ -4,13 +4,13 @@ return [
'not_found' => 'テーマが見つかりません: :theme',
'name_required' => 'テーマ名は必須です。',
'name_string' => 'テーマ名は文字列である必要があります。',
'name_max' => 'テーマ名は255文字を超えることができません。',
'name_max' => 'テーマ名は255文字を超えることはできません。',
'type_required' => 'テーマタイプは必須です。',
'type_invalid' => 'テーマタイプはcoreまたはmoduleである必要があります。',
'target_required' => 'テーマターゲットは必須です。',
'target_invalid' => 'テーマターゲットはadminまたはuserである必要があります。',
'module_name_required' => 'モジュールテーマの場合、モジュール名が必要です。',
'module_name_string' => 'モジュール名は文字列である必要があります。',
'module_name_max' => 'モジュール名は255文字を超えることができません。',
'module_name_max' => 'モジュール名は255文字を超えることはできません。',
'cannot_uninstall_active' => '有効化されたテーマは削除できません。',
];
@@ -3,7 +3,7 @@
return [
'status' => [
'active' => '有効',
'inactive' => '無効化',
'inactive' => '無効',
'blocked' => 'ブロック',
'withdrawn' => '退会',
],
@@ -13,35 +13,35 @@ return [
],
'not_found' => 'ユーザーが見つかりません。',
'withdrawn_user' => '退会したユーザー',
'fetch_success' => 'ユーザー情報の取得に成功しました。',
'fetch_success' => 'ユーザー情報を正常に取得しました。',
'fetch_failed' => 'ユーザー情報の取得に失敗しました。',
'create_success' => 'ユーザーが正常に作成されました。',
'create_failed' => 'ユーザーの作成に失敗しました: :error',
'create_failed' => 'ユーザー作成に失敗しました: :error',
'update_success' => 'ユーザー情報が正常に更新されました。',
'update_failed' => 'ユーザー情報の更新に失敗しました: :error',
'delete_success' => 'ユーザーが正常に削除されました。',
'delete_failed' => 'ユーザーの削除に失敗しました: :error',
'statistics_success' => 'ユーザー統計の取得に成功しました。',
'delete_failed' => 'ユーザー削除に失敗しました: :error',
'statistics_success' => 'ユーザー統計を正常に取得しました。',
'statistics_failed' => 'ユーザー統計の取得に失敗しました。',
'search_success' => 'ユーザー検索が正常に完了しました。',
'search_failed' => 'ユーザー検索に失敗しました。',
'search_name_required' => '検索する名前を入力してください。',
'search_keyword_required' => '検索キーワードを入力してください。',
'email_available' => '使用可能なメールアドレスです。',
'email_unavailable' => 'すでに使用されているメールアドレスです。',
'email_unavailable' => '既に使用中のメールアドレスです。',
'email_required' => 'メールアドレスを入力してください。',
'email_check_failed' => 'メールアドレスの重複確認に失敗しました。',
'profile_success' => 'プロフィール情報の取得に成功しました。',
'email_check_failed' => 'メールアドレス重複確認に失敗しました。',
'profile_success' => 'プロフィール情報を正常に取得しました。',
'profile_failed' => 'プロフィール情報の取得に失敗しました。',
'language_update_success' => '言語設定が正常に変更されました。',
'language_update_failed' => '言語設定の変更に失敗しました。',
'invalid_language' => 'サポートされていない言語です。',
'activity_log_success' => 'アクティビティログの取得に成功しました。',
'activity_log_success' => 'アクティビティログを正常に取得しました。',
'activity_log_failed' => 'アクティビティログの取得に失敗しました。',
'bulk_status_updated' => ':count名のユーザーステータスが変更されました。',
'bulk_update_status_failed' => 'ユーザーステータスの一括変更に失敗しました。',
'last_admin_role_cannot_remove' => 'システムに管理者ロールを持つユーザーが本人のみのため、管理者ロールを削除できません。',
'avatar_upload_success' => 'プロフィール画像のアップロードに成功しました。',
'bulk_update_status_failed' => 'ユーザーステータス一括変更に失敗しました。',
'last_admin_role_cannot_remove' => 'システムに管理者ロールを持つユーザーが本人のみであるため、管理者ロールを削除することはできません。',
'avatar_upload_success' => 'プロフィール画像が正常にアップロードされました。',
'avatar_upload_failed' => 'プロフィール画像のアップロードに失敗しました。',
'avatar_delete_success' => 'プロフィール画像が正常に削除されました。',
'avatar_delete_failed' => 'プロフィール画像の削除に失敗しました。',
@@ -52,7 +52,7 @@ return [
'password_incorrect' => 'パスワードが一致しません。',
'password_verify_failed' => 'パスワード確認に失敗しました。',
'password_change_success' => 'パスワードが正常に変更されました。',
'password_change_failed' => 'パスワードの変更に失敗しました。',
'password_change_failed' => 'パスワード変更に失敗しました。',
'withdraw_success' => '会員退会が完了しました。',
'withdraw_failed' => '会員退会に失敗しました: :error',
'withdraw_admin_forbidden' => '管理者アカウントは退会できません。',
@@ -62,12 +62,12 @@ return [
'per_page_integer' => 'ページあたりの項目数は数字である必要があります。',
'per_page_min' => 'ページあたりの項目数は1以上である必要があります。',
'per_page_max' => 'ページあたりの項目数は100以下である必要があります。',
'search_string' => '検索語は文字列である必要があります。',
'search_max' => '検索語は255文字以下である必要があります。',
'search_string' => '検索キーワードは文字列である必要があります。',
'search_max' => '検索キーワードは255文字以下である必要があります。',
'search_field_invalid' => '無効な検索フィールドです。',
'start_date_invalid' => '開始日の形式が正しくありません。',
'end_date_invalid' => '終了日の形式が正しくありません。',
'end_date_after_start' => '終了日は開始日より遅い必要があります。',
'start_date_invalid' => '開始日付の形式が正しくありません。',
'end_date_invalid' => '終了日付の形式が正しくありません。',
'end_date_after_start' => '終了日付は開始日付より後である必要があります。',
'date_filter_invalid' => '無効な日付フィルターです。',
'sort_by_invalid' => '無効な並べ替えフィールドです。',
'sort_order_invalid' => '無効な並べ替え順序です。',
@@ -78,34 +78,34 @@ return [
'nickname_max' => 'ニックネームは50文字以下である必要があります。',
'email_required' => 'メールアドレスは必須です。',
'email_invalid' => '正しいメールアドレス形式ではありません。',
'email_unique' => 'すでに使用されているメールアドレスです。',
'email_unique' => '既に使用中のメールアドレスです。',
'email_max' => 'メールアドレスは255文字以下である必要があります。',
'password_required' => 'パスワードは必須です。',
'password_confirmed' => 'パスワード確認が一致しません。',
'password_min' => 'パスワードは最低8文字以上である必要があります。',
'password_min' => 'パスワードは最小8文字以上である必要があります。',
'current_password_required' => '現在のパスワードを入力してください。',
'current_password_invalid' => '現在のパスワードが正しくありません。',
'password_confirmation_required' => 'パスワード確認を入力してください。',
'language_required' => '言語を選択してください。',
'language_invalid' => 'サポートされていない言語です。',
'country_required' => '国を選択してください。',
'country_size' => '国コードは2文字である必要があります。',
'country_alpha' => '国コードは英字のみ使用可能です。',
'country_size' => '国コードは2桁である必要があります。',
'country_alpha' => '国コードは英文字のみ使用可能です。',
'timezone_invalid' => '有効なタイムゾーンを選択してください。',
'status_invalid' => '無効なアカウント状態です。',
'homepage_url' => 'URLの形式が正しくありません。',
'homepage_url' => '正しいURL形式ではありません。',
'homepage_max' => 'ホームページアドレスは255文字以下である必要があります。',
'mobile_max' => '携帯電話番号は20文字以下である必要があります。',
'mobile_regex' => '携帯電話番号は数字、ハイフン、空白のみ入力可能です。',
'mobile_regex' => '携帯電話番号は数字、ハイフン、スペースのみ入力可能です。',
'phone_max' => '電話番号は20文字以下である必要があります。',
'phone_regex' => '電話番号は数字、ハイフン、空白のみ入力可能です。',
'phone_regex' => '電話番号は数字、ハイフン、スペースのみ入力可能です。',
'avatar_required' => 'プロフィール画像を選択してください。',
'avatar_image' => 'プロフィール画像は画像ファイルである必要があります。',
'avatar_mimes' => 'プロフィール画像はJPG、PNG、GIF、WEBP形式のみ対応しています。',
'avatar_mimes' => 'プロフィール画像はJPG、PNG、GIF、WEBP形式のみ可能です。',
'avatar_max' => 'プロフィール画像は2MB以下である必要があります。',
'zipcode_max' => '郵便番号は10文字以下である必要があります。',
'address_max' => '基本アドレスは255文字以下である必要があります。',
'address_detail_max' => '詳細アドレスは255文字以下である必要があります。',
'address_max' => '基本住所は255文字以下である必要があります。',
'address_detail_max' => '詳細住所は255文字以下である必要があります。',
'signature_max' => '署名は1000文字以下である必要があります。',
'bio_max' => '自己紹介は5000文字以下である必要があります。',
'admin_memo_max' => '管理者メモは5000文字以下である必要があります。',
File diff suppressed because it is too large Load Diff
@@ -3,18 +3,18 @@
return [
'mode' => [
'auto' => '自動 (推奨)',
'composer' => 'Composer を実行',
'bundled' => 'バンドル Vendor を使用',
'composer' => 'Composer 実行',
'bundled' => 'バンドル Vendor 使用',
],
'installer' => [
'checking_bundle' => 'Vendor バンドルを検証中...',
'extracting_bundle' => 'Vendor バンドルを抽出中 ({current}/{total})',
'running_composer' => 'Composer install を実行中...',
'checking_bundle' => 'Vendor バンドル検証中...',
'extracting_bundle' => 'Vendor バンドル抽出中 ({current}/{total})',
'running_composer' => 'Composer install 実行中...',
'mode_label' => 'Vendor インストール方式',
'mode_hint' => 'Composer が使用できない環境ではバンドルモードを選択してください。',
],
'build' => [
'start' => '{target} をビルド中...',
'start' => '{target} ビルド中...',
'success' => '{target} ビルド完了 ({size}, {packages} packages)',
'skipped_no_deps' => '{target} スキップ (外部 composer 依存関係なし)',
'skipped_not_installed' => '{target} スキップ (拡張がインストールされていません)',
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,13 @@
{
"mail.provider_default": {
"definition": {
"name": "メール本人確認 (デフォルト)",
"description": "特定の目的がマッチしない場合に使用されるデフォルトのメールテンプレート"
"name": "メール本人確認(基本)",
"description": "特定の目的がマッチしない場合に使用されるデフォルトメールテンプレート"
},
"templates": {
"mail": {
"subject": "[{app_name}] 本人確認認証コード",
"body": "<p>こんにちは。</p><p>下記の認証コードを入力して本人確認を完了してください。</p><p style=\"font-size:24px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>このコードは<strong>{expire_minutes}分</strong>後に有効期限切れになります。</p><p>本人が申請していない場合は、このメールを無視してください。</p><p>ご利用ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
"body": "<p>こんにちは。</p><p>下記の認証コードを入力して本人確認を完了してください。</p><p style=\"font-size:24px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>このコードは<strong>{expire_minutes}分</strong>後に失効します。</p><p>本人が要求していない場合は、このメールを無視してください。</p><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
}
}
},
@@ -19,19 +19,19 @@
"templates": {
"mail": {
"subject": "[{app_name}] 会員登録認証コード",
"body": "<h1>{app_name} 会員登録認証</h1><p>下記の認証コードを会員登録画面に入力してください。</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>このコードは<strong>{expire_minutes}分</strong>後に有効期限切れになります。</p><p>本人が登録を試行していない場合は、このメールを無視してください。</p><p>ご利用ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
"body": "<h1>{app_name} 会員登録認証</h1><p>下記の認証コードを会員登録画面に入力してください。</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>このコードは<strong>{expire_minutes}分</strong>後に失効します。</p><p>本人が登録を試みていない場合は、このメールを無視してください。</p><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
}
}
},
"mail.purpose.password_reset": {
"definition": {
"name": "パスワード再設定リンク",
"description": "パスワード再設定リクエスト時に送信される署名付きリンクメール (link フロー)"
"description": "パスワード再設定要求時に送信される署名付きリンクメール(link フロー)"
},
"templates": {
"mail": {
"subject": "[{app_name}] パスワード再設定のご案内",
"body": "<h1>パスワード再設定</h1><p>パスワード再設定のリクエストを受け取りました。下記のボタンをクリックしてパスワードを再設定してください。</p><p>このリンクは<strong>{expire_minutes}分</strong>後に有効期限切れになります。</p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:24px 0;\"><tr><td align=\"center\"><a href=\"{action_url}\" style=\"display:inline-block; padding:12px 32px; background-color:#2d3748; color:#ffffff; text-decoration:none; border-radius:4px; font-weight:600; font-size:14px;\">パスワード再設定</a></td></tr></table><p>本人が申請していない場合は、このメールを無視してください。</p><p>ご利用ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
"subject": "[{app_name}] パスワード再設定のお知らせ",
"body": "<h1>パスワード再設定</h1><p>パスワード再設定のリクエストを受け取りました。下記のボタンをクリックしてパスワードを再設定してください。</p><p>このリンクは<strong>{expire_minutes}分</strong>後に失効します。</p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:24px 0;\"><tr><td align=\"center\"><a href=\"{action_url}\" style=\"display:inline-block; padding:12px 32px; background-color:#2d3748; color:#ffffff; text-decoration:none; border-radius:4px; font-weight:600; font-size:14px;\">パスワードを再設定</a></td></tr></table><p>本人が要求していない場合は、このメールを無視してください。</p><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
}
}
},
@@ -43,19 +43,19 @@
"templates": {
"mail": {
"subject": "[{app_name}] アカウント情報変更認証コード",
"body": "<h1>アカウント情報変更認証</h1><p>{purpose_label}の処理のために下記の認証コードを入力してください。</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>このコードは<strong>{expire_minutes}分</strong>後に有効期限切れになります。</p><p><strong>本人が変更を試行していない場合は、直ちにパスワードを変更してください。</strong></p><p>ご利用ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
"body": "<h1>アカウント情報変更認証</h1><p>{purpose_label}の処理のため、下記の認証コードを入力してください。</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>このコードは<strong>{expire_minutes}分</strong>後に失効します。</p><p><strong>本人が変更を試みていない場合は、直ちにパスワードを変更してください。</strong></p><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
}
}
},
"mail.purpose.sensitive_action": {
"definition": {
"name": "重要な作業認証",
"description": "決済、重要な設定変更など重要な作業時の本人確認コードメール"
"description": "決済や主要な設定変更など重要な作業時の本人確認コードメール"
},
"templates": {
"mail": {
"subject": "[{app_name}] 重要な作業認証コード",
"body": "<h1>重要な作業認証</h1><p>{purpose_label}の進行のために下記の認証コードを入力してください。</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>このコードは<strong>{expire_minutes}分</strong>後に有効期限切れになります。</p><p><strong>本人が試行していない場合は、直ちにカスタマーサポートチームにお問い合わせください。</strong></p><p>ご利用ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
"body": "<h1>重要な作業認証</h1><p>{purpose_label}の進行のため、下記の認証コードを入力してください。</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>このコードは<strong>{expire_minutes}分</strong>後に失効します。</p><p><strong>本人が試みていない場合は、直ちにカスタマーサポートにお問い合わせください。</strong></p><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
}
}
}
@@ -1,33 +1,33 @@
{
"welcome": {
"definition": {
"name": "会員登録 ウェルカム",
"name": "会員登録ウェルカム",
"description": "会員登録完了時に送信されるウェルカム通知"
},
"templates": {
"mail": {
"subject": "[{app_name}] 会員登録をお待ちしておりました",
"body": "<h1>{name}様、ようこそ!</h1><p>{app_name}にご登録いただきありがとうございます。</p><p>これで全てのサービスをご利用いただけます。以下のボタンをクリックしてログインしてください。</p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{action_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">ログインする</a></td></tr></table><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
"subject": "[{app_name}] 会員登録をお待ちしていました",
"body": "<h1>{name}様、ようこそ!</h1><p>{app_name}へのご登録ありがとうございます。</p><p>これですべてのサービスをご利用いただけます。下のボタンをクリックしてログインしてください。</p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{action_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">ログインする</a></td></tr></table><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
},
"database": {
"subject": "会員登録をお待ちしておりました",
"body": "{name}様、{app_name}にご登録いただきありがとうございます。"
"subject": "会員登録をお待ちしていました",
"body": "{name}様、{app_name}へのご登録ありがとうございます。"
}
}
},
"reset_password": {
"definition": {
"name": "パスワード再設定",
"description": "パスワード再設定要求時に送信される通知"
"name": "パスワードリセット",
"description": "パスワードリセット要求時に送信される通知"
},
"templates": {
"mail": {
"subject": "[{app_name}] パスワード再設定のご案内",
"body": "<h1>{name}様、こんにちは。</h1><p>パスワード再設定のご要望を承りました。以下のボタンをクリックしてパスワードを再設定してください。</p><p>このリンクは<strong>{expire_minutes}分</strong>後に期限切れになります。</p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{action_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">パスワード再設定</a></td></tr></table><p>パスワード再設定をご要望でなかった場合は、このメールを無視してください。</p><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
"subject": "[{app_name}] パスワードリセットのご案内",
"body": "<h1>{name}様、こんにちは。</h1><p>パスワードリセットのご要求をお受けしました。下のボタンをクリックしてパスワードをリセットしてください。</p><p>このリンクは<strong>{expire_minutes}分</strong>後に有効期限切れになります。</p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{action_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">パスワードをリセットする</a></td></tr></table><p>パスワードリセットのご要求をされていない場合は、このメールを無視してください。</p><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
},
"database": {
"subject": "パスワード再設定のご案内",
"body": "{name}様、パスワード再設定が要求されました。ご本人様による操作でない場合は、この通知を無視してください。"
"subject": "パスワードリセットのご案内",
"body": "{name}様、パスワードリセットが要求されました。ご自身で要求されていない場合は、この通知を無視してください。"
}
}
},
@@ -39,11 +39,11 @@
"templates": {
"mail": {
"subject": "[{app_name}] パスワードが変更されました",
"body": "<h1>{name}様、こんにちは。</h1><p>アカウントのパスワードが正常に変更されました。</p><p><strong>ご本人様による操作でない場合は、直ちにカスタマーサポートまでお問い合わせください。</strong></p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{action_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">ログインする</a></td></tr></table><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
"body": "<h1>{name}様、こんにちは。</h1><p>アカウントのパスワードが正常に変更されました。</p><p><strong>ご自身で変更されていない場合は、すぐにカスタマーサポートにお問い合わせください。</strong></p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{action_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">ログインする</a></td></tr></table><p>ありがとうございます。<br><a href=\"{site_url}\">{app_name}</a></p>"
},
"database": {
"subject": "パスワードが変更されました",
"body": "{name}様、パスワードが変更されました。ご本人様による操作でない場合は、直ちにカスタマーサポートまでお問い合わせください。"
"body": "{name}様、パスワードが変更されました。ご自身で変更されていない場合は、すぐにカスタマーサポートにお問い合わせください。"
}
}
}
@@ -8,8 +8,8 @@
"description": "ユーザー管理権限"
},
"core.users.read": {
"name": "ユーザー조회",
"description": "ユーザー一覧および詳細情報を閲覧できます。"
"name": "ユーザー閲覧",
"description": "ユーザーリストと詳細情報を閲覧できます。"
},
"core.users.create": {
"name": "ユーザー作成",
@@ -28,8 +28,8 @@
"description": "メニュー管理権限"
},
"core.menus.read": {
"name": "メニュー조회",
"description": "メニュー一覧を閲覧できます。"
"name": "メニュー閲覧",
"description": "メニューリストを閲覧できます。"
},
"core.menus.create": {
"name": "メニュー作成",
@@ -48,8 +48,8 @@
"description": "モジュール管理権限"
},
"core.modules.read": {
"name": "モジュール조회",
"description": "モジュール一覧を閲覧できます。"
"name": "モジュール閲覧",
"description": "モジュールリストを閲覧できます。"
},
"core.modules.install": {
"name": "モジュールインストール",
@@ -68,8 +68,8 @@
"description": "プラグイン管理権限"
},
"core.plugins.read": {
"name": "プラグイン조회",
"description": "プラグイン一覧を閲覧できます。"
"name": "プラグイン閲覧",
"description": "プラグインリストを閲覧できます。"
},
"core.plugins.install": {
"name": "プラグインインストール",
@@ -92,8 +92,8 @@
"description": "テンプレート管理権限"
},
"core.templates.read": {
"name": "テンプレート조회",
"description": "テンプレート一覧を閲覧できます。"
"name": "テンプレート閲覧",
"description": "テンプレートリストを閲覧できます。"
},
"core.templates.install": {
"name": "テンプレートインストール",
@@ -116,8 +116,8 @@
"description": "ロールおよび権限管理権限"
},
"core.permissions.read": {
"name": "権限조회",
"description": "ロールおよび権限一覧を閲覧できます。"
"name": "権限閲覧",
"description": "ロールおよび権限リストを閲覧できます。"
},
"core.permissions.create": {
"name": "ロール作成",
@@ -125,7 +125,7 @@
},
"core.permissions.update": {
"name": "ロール編集",
"description": "ロール情報および権限を編集できます。"
"description": "ロール情報と権限を編集できます。"
},
"core.permissions.delete": {
"name": "ロール削除",
@@ -136,7 +136,7 @@
"description": "通知発送履歴管理権限"
},
"core.notification-logs.read": {
"name": "発送履歴조회",
"name": "発送履歴閲覧",
"description": "通知発送履歴を閲覧できます。"
},
"core.notification-logs.delete": {
@@ -145,11 +145,11 @@
},
"core.notifications": {
"name": "通知(管理者)",
"description": "管理者用通知管理権限(管理者画面で使用)"
"description": "管理者用通知管理権限(管理画面で使用)"
},
"core.notifications.read": {
"name": "通知조회",
"description": "通知一覧および未読数を閲覧できます。"
"name": "通知閲覧",
"description": "通知リストと未読数を閲覧できます。"
},
"core.notifications.update": {
"name": "通知既読処理",
@@ -161,11 +161,11 @@
},
"core.user-notifications": {
"name": "通知 (ユーザー)",
"description": "ユーザー用通知権限 (ユーザー画面で本人通知管理)"
"description": "ユーザー向け通知権限 (ユーザー画面で本人の通知を管理)"
},
"core.user-notifications.read": {
"name": "通知閲覧",
"description": "本人の通知リスト及び未読数を閲覧できます。"
"description": "本人の通知一覧および未読数を閲覧できます。"
},
"core.user-notifications.update": {
"name": "通知読了処理",
@@ -176,56 +176,56 @@
"description": "本人の通知を削除できます。"
},
"core.identity": {
"name": "本人確認 (ユーザー)",
"description": "ログインユーザーの本人確認チャレンジリクエスト/検証/キャンセル権限"
"name": "本人認証 (ユーザー)",
"description": "ログインユーザーの本人認証チャレンジ要求/検証/キャンセル権限"
},
"core.identity.request": {
"name": "IDVリクエスト",
"description": "本人確認チャレンジをリクエストできます。"
"name": "IDV要求",
"description": "本人認証チャレンジを要求できます。"
},
"core.identity.verify": {
"name": "IDV検証",
"description": "本人確認チャレンジを検証できます。"
"description": "本人の本人認証チャレンジを検証できます。"
},
"core.identity.cancel": {
"name": "IDVキャンセル",
"description": "本人の本人確認チャレンジをキャンセルできます。"
"description": "本人の本人認証チャレンジをキャンセルできます。"
},
"core.admin.identity": {
"name": "本人確認管理 (管理者)",
"name": "本人認証管理 (管理者)",
"description": "管理者用IDVプロバイダ/ポリシー/ログ管理権限"
},
"core.admin.identity.providers.read": {
"name": "プロバイダ設定閲覧",
"description": "本人確認プロバイダ設定を閲覧できます。"
"description": "本人認証プロバイダ設定を閲覧できます。"
},
"core.admin.identity.providers.update": {
"name": "プロバイダ設定編集",
"description": "本人確認プロバイダ設定を編集できます。"
"description": "本人認証プロバイダ設定を編集できます。"
},
"core.admin.identity.policies.read": {
"name": "ポリシー閲覧",
"description": "本人確認ポリシー(ルート/フック別)を閲覧できます。"
"description": "本人認証ポリシー(ルート/フック別)を閲覧できます。"
},
"core.admin.identity.policies.update": {
"name": "ポリシー編集",
"description": "本人確認ポリシー(ルート/フック別)をCRUD できます。"
"description": "本人認証ポリシー(ルート/フック別)をCRUD できます。"
},
"core.admin.identity.logs.read": {
"name": "ログ閲覧",
"description": "本人確認履歴を閲覧できます。"
"description": "本人認証履歴を閲覧できます。"
},
"core.admin.identity.logs.purge": {
"name": "ログ破棄",
"description": "本人確認履歴を破棄(保管期間外)できます。"
"name": "ログ削除",
"description": "本人認証履歴を削除(保管周期外)できます。"
},
"core.admin.identity.messages.read": {
"name": "メッセージテンプレート閲覧",
"description": "本人確認メッセージ定義/テンプレートを閲覧できます。"
"description": "本人認証メッセージ定義/テンプレートを閲覧できます。"
},
"core.admin.identity.messages.update": {
"name": "メッセージテンプレート編集",
"description": "本人確認メッセージ定義/テンプレートを編集できます。"
"description": "本人認証メッセージ定義/テンプレートを編集できます。"
},
"core.settings": {
"name": "環境設定",
@@ -245,19 +245,19 @@
},
"core.dashboard.read": {
"name": "ダッシュボード閲覧",
"description": "ダッシュボード統計及び情報を閲覧できます。"
"description": "ダッシュボード統計および情報を閲覧できます。"
},
"core.dashboard.system-status": {
"name": "システム状態",
"description": "システム状態情報を閲覧できます。"
"name": "システムステータス",
"description": "システムステータス情報を閲覧できます。"
},
"core.dashboard.resources": {
"name": "システムリソース",
"description": "CPU、メモリ、ディスク使用量を閲覧できます。"
},
"core.dashboard.activities": {
"name": "最近の活動",
"description": "最近の活動履歴を閲覧できます。"
"name": "最近のアクティビティ",
"description": "最近のアクティビティ履歴を閲覧できます。"
},
"core.dashboard.alerts": {
"name": "システム通知",
@@ -285,7 +285,7 @@
},
"core.attachments.update": {
"name": "添付ファイル編集",
"description": "添付ファイル情報及び順序を編集できます。"
"description": "添付ファイル情報および順序を編集できます。"
},
"core.attachments.delete": {
"name": "添付ファイル削除",
@@ -297,7 +297,7 @@
},
"core.schedules.read": {
"name": "スケジュール閲覧",
"description": "スケジュールリスト及び詳細情報を閲覧できます。"
"description": "スケジュール一覧および詳細情報を閲覧できます。"
},
"core.schedules.create": {
"name": "スケジュール作成",
@@ -317,11 +317,11 @@
},
"core.language_packs": {
"name": "言語パック管理",
"description": "言語パックインストール/削除/有効化権限"
"description": "言語パックインストール/アンインストール/有効化権限"
},
"core.language_packs.read": {
"name": "言語パック閲覧",
"description": "インストール済み言語パックリスト及び詳細情報を閲覧できます。"
"description": "インストールされた言語パックの一覧と詳細情報を閲覧できます。"
},
"core.language_packs.install": {
"name": "言語パックインストール",
@@ -332,7 +332,7 @@
"description": "言語パックを有効化/無効化/削除できます。"
},
"core.language_packs.update": {
"name": "言語パック アップデート",
"name": "言語パックアップデート",
"description": "インストールされた言語パックのアップデートを確認して実行できます。"
}
}
@@ -9,10 +9,10 @@
},
"user": {
"name": "一般ユーザー",
"description": "基本的なユーザーロールです。"
"description": "基本ユーザーロールです。"
},
"guest": {
"name": "非会員",
"description": "認証されていないユーザーロールです。管理者が権限を付与することができます。"
"description": "認証されていないユーザーロールです。管理者が権限を付与できます。"
}
}
@@ -6,6 +6,11 @@
## [1.0.4] - 2026-07-17
### Added
- 환경설정 > SEO 화면에 추가된 Sitemap 분할 기준(파일당 URL 수)·Sitemap 압축 항목의 이름·설명 일본어 번역 추가 — 새 항목이 일본어 로케일에서도 올바르게 표시됩니다.
- 고급 설정에 추가된 Sitemap 캐시 유지 시간 항목의 이름·설명 일본어 번역 추가 — 새 항목이 일본어 로케일에서도 올바르게 표시됩니다.
### Changed
- 환경설정 > SEO 화면의 캐시 항목(SEO 캐시 활성화·캐시 유지 시간, Sitemap 캐시 유지 시간) 안내 문구 일본어 번역을 갱신했습니다 — 비워두면 고급 설정의 값을 따르고 값을 넣으면 그 값이 우선한다는 새 동작을 일본어 로케일에서도 올바르게 안내합니다.
@@ -1429,7 +1429,11 @@
"sitemap_manual_update": "今すぐ生成",
"sitemap_updating": "生成中...",
"sitemap_update_success": "Sitemap 生成が完了しました。",
"sitemap_update_failed": "Sitemap 生成に失敗しました。"
"sitemap_update_failed": "Sitemap 生成に失敗しました。",
"sitemap_urls_per_file": "Sitemap ファイルあたりの URL 数",
"sitemap_urls_per_file_hint": "URL がこの数を超えると sitemap ファイルが複数に分割され、インデックスファイルが一緒に作成されます — 最小 1000 個、最大 50000 個(検索エンジン仕様上限)",
"sitemap_gzip": "Sitemap 圧縮(gzip)",
"sitemap_gzip_desc": "分割された sitemap ファイルを圧縮して送信量を削減します。インデックスファイルは圧縮しません。"
},
"security": {
"title": "セキュリティ 設定",
@@ -1736,7 +1740,9 @@
"geoip_manual_update": "今すぐ更新",
"geoip_updating": "更新中...",
"geoip_update_success": "GeoIP DBの更新が完了しました。",
"geoip_update_failed": "GeoIP DBの更新に失敗しました。"
"geoip_update_failed": "GeoIP DBの更新に失敗しました。",
"seo_sitemap_cache": "Sitemap キャッシュ",
"seo_sitemap_cache_desc": "sitemap.xml をキャッシュします。"
},
"notification_definitions": {
"title": "通知設定",
+5
View File
@@ -12,9 +12,14 @@ return [
// Sitemap manual regeneration
'sitemap_regenerated' => 'Sitemap regeneration completed.',
'sitemap_regenerate_dispatched' => 'Sitemap regeneration has started. It may take a while to finish.',
'sitemap_regenerate_failed' => 'Sitemap regeneration failed.',
'sitemap_disabled' => 'Sitemap generation is disabled.',
// Sitemap generation/serving
'sitemap_not_ready' => 'The sitemap is being prepared. Please try again shortly.',
'sitemap_generate_failed' => 'Sitemap generation failed: :error',
// Cache stats
'stats_period' => 'SEO cache statistics for the last :days days',
'stats_overall' => 'Overall Statistics',
+17 -6
View File
@@ -865,12 +865,16 @@ return [
'stats_cache_ttl_integer' => 'Stats cache TTL must be an integer.',
'stats_cache_ttl_min' => 'Stats cache TTL must be at least 0 seconds.',
'stats_cache_ttl_max' => 'Stats cache TTL may not be greater than 14400 seconds (4 hours).',
'seo_cache_enabled_required' => 'Please select the SEO cache setting.',
'seo_cache_enabled_boolean' => 'SEO cache must be true or false.',
'seo_cache_ttl_required' => 'Please enter the SEO cache TTL.',
'seo_cache_ttl_integer' => 'SEO cache TTL must be an integer.',
'seo_cache_ttl_min' => 'SEO cache TTL must be at least 0 seconds.',
'seo_cache_ttl_max' => 'SEO cache TTL may not be greater than 14400 seconds (4 hours).',
// Advanced tab only — the SEO tab override field (seo_cache_*) allows a different range.
'advanced_seo_cache_enabled_required' => 'Please select the SEO cache setting.',
'advanced_seo_cache_enabled_boolean' => 'SEO cache must be true or false.',
'advanced_seo_cache_ttl_required' => 'Please enter the SEO cache TTL.',
'advanced_seo_cache_ttl_integer' => 'SEO cache TTL must be an integer.',
'advanced_seo_cache_ttl_min' => 'SEO cache TTL must be at least 0 seconds.',
'advanced_seo_cache_ttl_max' => 'SEO cache TTL may not be greater than 14400 seconds (4 hours).',
'seo_sitemap_cache_ttl_integer' => 'Sitemap cache TTL must be an integer.',
'seo_sitemap_cache_ttl_min' => 'Sitemap cache TTL must be at least 3600 seconds (1 hour).',
'seo_sitemap_cache_ttl_max' => 'Sitemap cache TTL may not be greater than 604800 seconds (7 days).',
// Debug settings
'debug_mode_required' => 'Please select the debug mode setting.',
@@ -954,6 +958,13 @@ return [
'sitemap_cache_ttl_integer' => 'Sitemap cache TTL must be an integer.',
'sitemap_cache_ttl_min' => 'Sitemap cache TTL must be at least 3600 seconds (1 hour).',
'sitemap_cache_ttl_max' => 'Sitemap cache TTL may not be greater than 604800 seconds (7 days).',
'sitemap_urls_per_file_integer' => 'URLs per sitemap file must be an integer.',
'sitemap_urls_per_file_min' => 'URLs per sitemap file must be at least 1000.',
'sitemap_urls_per_file_max' => 'URLs per sitemap file may not be greater than 50000.',
'sitemap_gzip_boolean' => 'Sitemap compression setting must be true or false.',
'sitemap_serve_stale_on_miss_boolean' => 'Serving the previous sitemap setting must be true or false.',
'sitemap_max_urls_per_contributor_integer' => 'Maximum URLs per contributor must be an integer.',
'sitemap_max_urls_per_contributor_min' => 'Maximum URLs per contributor must be at least 0.',
'sitemap_schedule_invalid' => 'Please select a valid sitemap generation schedule.',
'sitemap_schedule_time_invalid' => 'Sitemap generation time must be in HH:mm format.',
+5
View File
@@ -12,9 +12,14 @@ return [
// Sitemap 수동 재생성
'sitemap_regenerated' => 'Sitemap 재생성이 완료되었습니다.',
'sitemap_regenerate_dispatched' => 'Sitemap 재생성을 시작했습니다. 완료까지 시간이 걸릴 수 있습니다.',
'sitemap_regenerate_failed' => 'Sitemap 재생성에 실패했습니다.',
'sitemap_disabled' => 'Sitemap 생성이 비활성화되어 있습니다.',
// Sitemap 생성/서빙
'sitemap_not_ready' => 'Sitemap 을 준비하는 중입니다. 잠시 후 다시 시도해주세요.',
'sitemap_generate_failed' => 'Sitemap 생성에 실패했습니다: :error',
// 캐시 통계
'stats_period' => '최근 :days일간 SEO 캐시 통계',
'stats_overall' => '전체 통계',
+17 -6
View File
@@ -903,6 +903,13 @@ return [
'sitemap_cache_ttl_integer' => 'Sitemap 캐시 TTL은 정수여야 합니다.',
'sitemap_cache_ttl_min' => 'Sitemap 캐시 TTL은 최소 3600초(1시간) 이상이어야 합니다.',
'sitemap_cache_ttl_max' => 'Sitemap 캐시 TTL은 최대 604800초(7일)를 초과할 수 없습니다.',
'sitemap_urls_per_file_integer' => 'Sitemap 파일당 URL 수는 정수여야 합니다.',
'sitemap_urls_per_file_min' => 'Sitemap 파일당 URL 수는 최소 1000개 이상이어야 합니다.',
'sitemap_urls_per_file_max' => 'Sitemap 파일당 URL 수는 최대 50000개를 초과할 수 없습니다.',
'sitemap_gzip_boolean' => 'Sitemap 압축 설정은 true 또는 false 값이어야 합니다.',
'sitemap_serve_stale_on_miss_boolean' => '이전 Sitemap 제공 설정은 true 또는 false 값이어야 합니다.',
'sitemap_max_urls_per_contributor_integer' => '수집기당 최대 URL 수는 정수여야 합니다.',
'sitemap_max_urls_per_contributor_min' => '수집기당 최대 URL 수는 0 이상이어야 합니다.',
'sitemap_schedule_invalid' => '유효한 Sitemap 생성 주기를 선택해주세요.',
'sitemap_schedule_time_invalid' => 'Sitemap 생성 시각은 HH:mm 형식이어야 합니다.',
@@ -938,12 +945,16 @@ return [
'stats_cache_ttl_integer' => '통계 캐시 만료 시간은 정수여야 합니다.',
'stats_cache_ttl_min' => '통계 캐시 만료 시간은 최소 0초여야 합니다.',
'stats_cache_ttl_max' => '통계 캐시 만료 시간은 최대 14400초(4시간)를 초과할 수 없습니다.',
'seo_cache_enabled_required' => 'SEO 캐시 설정을 선택해주세요.',
'seo_cache_enabled_boolean' => 'SEO 캐시는 true 또는 false 값이어야 합니다.',
'seo_cache_ttl_required' => 'SEO 캐시 만료 시간을 입력해주세요.',
'seo_cache_ttl_integer' => 'SEO 캐시 만료 시간은 정수여야 합니다.',
'seo_cache_ttl_min' => 'SEO 캐시 만료 시간은 최소 0초여야 합니다.',
'seo_cache_ttl_max' => 'SEO 캐시 만료 시간은 최대 14400초(4시간)를 초과할 수 없습니다.',
// 고급 탭 전용 — SEO 탭의 오버라이드 칸(seo_cache_*)과 허용 범위가 다르므로 키를 분리한다.
'advanced_seo_cache_enabled_required' => 'SEO 캐시 설정을 선택해주세요.',
'advanced_seo_cache_enabled_boolean' => 'SEO 캐시는 true 또는 false 값이어야 합니다.',
'advanced_seo_cache_ttl_required' => 'SEO 캐시 만료 시간을 입력해주세요.',
'advanced_seo_cache_ttl_integer' => 'SEO 캐시 만료 시간은 정수여야 합니다.',
'advanced_seo_cache_ttl_min' => 'SEO 캐시 만료 시간은 최소 0초여야 합니다.',
'advanced_seo_cache_ttl_max' => 'SEO 캐시 만료 시간은 최대 14400초(4시간)를 초과할 수 없습니다.',
'seo_sitemap_cache_ttl_integer' => 'Sitemap 캐시 만료 시간은 정수여야 합니다.',
'seo_sitemap_cache_ttl_min' => 'Sitemap 캐시 만료 시간은 최소 3600초(1시간) 이상이어야 합니다.',
'seo_sitemap_cache_ttl_max' => 'Sitemap 캐시 만료 시간은 최대 604800초(7일)를 초과할 수 없습니다.',
// 디버그 설정
'debug_mode_required' => '디버그 모드 설정을 선택해주세요.',
+12 -7
View File
@@ -1,5 +1,8 @@
<?php
use App\Extension\ModuleManager;
use App\Extension\PluginManager;
use App\Jobs\GenerateSitemapJob;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
@@ -42,8 +45,10 @@ if (file_exists(base_path('.env'))) {
$sitemapFrequency = g7_core_settings('seo.sitemap_schedule', 'daily');
$sitemapTime = g7_core_settings('seo.sitemap_schedule_time', '02:00');
$sitemapScheduled = Schedule::job(new \App\Jobs\GenerateSitemapJob)
->onOneServer();
// 대용량 생성은 스케줄 주기를 넘길 수 있으므로 이전 실행이 끝나기 전에는 겹쳐 돌지 않게 한다.
$sitemapScheduled = Schedule::job(new GenerateSitemapJob)
->onOneServer()
->withoutOverlapping(30);
match ($sitemapFrequency) {
'hourly' => $sitemapScheduled->hourly(),
@@ -89,7 +94,7 @@ if (file_exists(base_path('.env'))) {
if (file_exists(base_path('.env'))) {
try {
// 모듈 스케줄 등록
$moduleManager = app(\App\Extension\ModuleManager::class);
$moduleManager = app(ModuleManager::class);
foreach ($moduleManager->getActiveModules() as $module) {
foreach ($module->getSchedules() as $config) {
if (empty($config['command']) || empty($config['schedule'])) {
@@ -113,7 +118,7 @@ if (file_exists(base_path('.env'))) {
$settingKey = $config['enabled_config'];
// "identifier.key" 형식이면 identifier 부분 제거하여 설정 키만 추출
if (str_starts_with($settingKey, $identifier . '.')) {
if (str_starts_with($settingKey, $identifier.'.')) {
$settingKey = substr($settingKey, strlen($identifier) + 1);
}
@@ -123,7 +128,7 @@ if (file_exists(base_path('.env'))) {
}
// 플러그인 스케줄 등록
$pluginManager = app(\App\Extension\PluginManager::class);
$pluginManager = app(PluginManager::class);
foreach ($pluginManager->getActivePlugins() as $plugin) {
foreach ($plugin->getSchedules() as $config) {
if (empty($config['command']) || empty($config['schedule'])) {
@@ -146,7 +151,7 @@ if (file_exists(base_path('.env'))) {
$settingKey = $config['enabled_config'];
// "identifier.key" 형식이면 identifier 부분 제거하여 설정 키만 추출
if (str_starts_with($settingKey, $identifier . '.')) {
if (str_starts_with($settingKey, $identifier.'.')) {
$settingKey = substr($settingKey, strlen($identifier) + 1);
}
@@ -154,7 +159,7 @@ if (file_exists(base_path('.env'))) {
}
}
}
} catch (\Exception $e) {
} catch (Exception $e) {
Log::debug('확장 스케줄 등록 스킵', ['error' => $e->getMessage()]);
}
}
+4
View File
@@ -39,7 +39,11 @@ Route::prefix('admin')
});
// Sitemap XML 라우트
// 자식 경로는 SitemapFileStore::childUrl() 이 sitemapindex 의 <loc> 에 기록하는 값과 일치해야 한다.
// gzip 여부는 manifest 가 결정하므로 .xml / .xml.gz 를 같은 액션에서 처리한다.
Route::get('/sitemap.xml', [SitemapController::class, 'index'])->name('web.sitemap');
Route::get('/sitemap-{n}.xml', [SitemapController::class, 'child'])->whereNumber('n')->name('web.sitemap.child');
Route::get('/sitemap-{n}.xml.gz', [SitemapController::class, 'child'])->whereNumber('n')->name('web.sitemap.child.gz');
// User 라우트 - user 템플릿 의존성 검증 + SEO 봇 감지
Route::middleware(['template.dependencies:user', 'seo'])
@@ -4,6 +4,17 @@
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
## [1.0.4] - 2026-07-17
### Added
- SEO 설정에 Sitemap 분할 기준(파일당 URL 수)과 Sitemap 압축 항목을 추가했습니다. URL 이 분할 기준을 넘으면 sitemap 이 여러 파일로 나뉘어 생성됩니다.
- 고급 설정의 캐시 항목에 Sitemap 캐시 유지 시간을 추가했습니다. SEO 설정의 Sitemap 캐시 항목을 비워두면 이 값을 따릅니다.
### Changed
- SEO 설정의 "지금 생성" 버튼이 sitemap 생성을 예약하고 바로 응답합니다. 사이트 규모가 커도 화면이 멈추지 않으며, 생성 완료는 마지막 생성 시각으로 확인할 수 있습니다.
## [1.0.3] - 2026-07-14
### Fixed
@@ -0,0 +1,139 @@
/**
* @file admin-settings-sitemap-fields.test.tsx
* @description 환경설정 > SEO/고급 탭의 Sitemap 관련 칸 계약 테스트
*
* 검증 포인트:
* - SEO 탭: 분할 기준(sitemap_urls_per_file) / 압축(sitemap_gzip) 칸이 존재하고
* name 과 허용 범위가 백엔드 검증 규칙(SaveSettingsRequest)과 일치
* - 고급 탭: Sitemap 캐시 기준값(advanced.seo_sitemap_cache_ttl) 칸이 존재하고
* 범위가 SEO 탭 오버라이드 칸과 동일 (두 칸의 허용값이 어긋나면 한쪽 저장이 막힘)
* - 수동 재생성 버튼이 async 전환 이후에도 동일 엔드포인트를 호출
*/
import { describe, it, expect } from 'vitest';
const seoTab = require('../../layouts/partials/admin_settings/_tab_seo.json');
const advancedTab = require('../../layouts/partials/admin_settings/_tab_advanced.json');
/**
* 트리에서 조건에 맞는 노드를 모두 수집.
*
* @param node 시작 노드
* @param predicate 노드 매칭 조건
* @returns 매칭된 노드 배열
*/
function collectNodes(node: any, predicate: (n: any) => boolean): any[] {
const result: any[] = [];
const visit = (n: any) => {
if (!n || typeof n !== 'object') return;
if (Array.isArray(n)) {
n.forEach(visit);
return;
}
if (predicate(n)) result.push(n);
if (n.children) visit(n.children);
if (n.cellChildren) visit(n.cellChildren);
if (n.actions) visit(n.actions);
if (n.params) visit(n.params);
if (n.onSuccess) visit(n.onSuccess);
if (n.onError) visit(n.onError);
};
visit(node);
return result;
}
/**
* name prop 으로 폼 필드 노드 1개를 찾는다.
*
* @param tree 레이아웃 트리
* @param name 필드 name
* @returns 매칭 노드 (없으면 undefined)
*/
function findFieldByName(tree: any, name: string): any {
return collectNodes(tree, (n) => n?.props?.name === name)[0];
}
describe('환경설정 > SEO 탭 > Sitemap 생성 칸', () => {
it('분할 기준 칸이 seo.sitemap_urls_per_file 이름으로 존재한다', () => {
const field = findFieldByName(seoTab, 'seo.sitemap_urls_per_file');
expect(field).toBeDefined();
expect(field.name).toBe('Input');
expect(field.props.type).toBe('number');
});
it('분할 기준 칸의 허용 범위가 백엔드 검증 규칙(1000~50000)과 일치한다', () => {
const field = findFieldByName(seoTab, 'seo.sitemap_urls_per_file');
expect(field.props.min).toBe(1000);
expect(field.props.max).toBe(50000);
});
it('압축 칸이 seo.sitemap_gzip Toggle 로 존재한다', () => {
const field = findFieldByName(seoTab, 'seo.sitemap_gzip');
expect(field).toBeDefined();
expect(field.name).toBe('Toggle');
expect(field.type).toBe('composite');
});
it('신규 칸이 다국어 키로 라벨을 표시한다 (하드코딩 문구 없음)', () => {
const urlsPerFileField = collectNodes(
seoTab,
(n) => n.id === 'field_sitemap_urls_per_file'
)[0];
const gzipField = collectNodes(seoTab, (n) => n.id === 'field_sitemap_gzip')[0];
const labels = [
...collectNodes(urlsPerFileField, (n) => typeof n.text === 'string'),
...collectNodes(gzipField, (n) => typeof n.text === 'string'),
].map((n) => n.text);
expect(labels.length).toBeGreaterThan(0);
labels.forEach((text) => expect(text.startsWith('$t:')).toBe(true));
});
it('수동 재생성 버튼이 재생성 엔드포인트를 호출한다', () => {
const apiCalls = collectNodes(
seoTab,
(n) => n.handler === 'apiCall' && n.target === '/api/admin/seo/sitemap/regenerate'
);
expect(apiCalls.length).toBe(1);
expect(apiCalls[0].params.method).toBe('POST');
});
});
describe('환경설정 > 고급 탭 > Sitemap 캐시 기준값 칸', () => {
it('Sitemap 캐시 칸이 advanced.seo_sitemap_cache_ttl 이름으로 존재한다', () => {
const field = findFieldByName(advancedTab, 'advanced.seo_sitemap_cache_ttl');
expect(field).toBeDefined();
expect(field.name).toBe('Input');
expect(field.props.type).toBe('number');
});
it('Sitemap 캐시 칸의 허용 범위가 SEO 탭 오버라이드 칸과 동일하다', () => {
const advancedField = findFieldByName(advancedTab, 'advanced.seo_sitemap_cache_ttl');
const seoOverrideField = findFieldByName(seoTab, 'seo.sitemap_cache_ttl');
expect(Number(advancedField.props.min)).toBe(Number(seoOverrideField.props.min));
expect(Number(advancedField.props.max)).toBe(Number(seoOverrideField.props.max));
});
it('Sitemap 캐시 칸이 읽기 전용 상태와 검증 오류를 형제 칸과 동일하게 반영한다', () => {
const field = findFieldByName(advancedTab, 'advanced.seo_sitemap_cache_ttl');
const sibling = findFieldByName(advancedTab, 'advanced.seo_cache_ttl');
expect(field.props.disabled).toBe(sibling.props.disabled);
expect(field.props.className).toContain("_local.errors?.['advanced.seo_sitemap_cache_ttl']");
const errorNode = collectNodes(
advancedTab,
(n) =>
typeof n.if === 'string' &&
n.if.includes("_local.errors?.['advanced.seo_sitemap_cache_ttl']")
)[0];
expect(errorNode).toBeDefined();
});
});
@@ -1423,9 +1423,13 @@
"sitemap_cache_ttl_hint": "Leave empty to follow the sitemap cache TTL in Advanced settings. Enter a value to override it here — min 3600s (1 hour), max 604800s (7 days)",
"sitemap_last_updated_at": "Last generated",
"sitemap_manual_update": "Generate now",
"sitemap_updating": "Generating...",
"sitemap_update_success": "Sitemap generation completed.",
"sitemap_update_failed": "Sitemap generation failed."
"sitemap_updating": "Starting...",
"sitemap_update_success": "Sitemap regeneration has started. It may take a while to finish.",
"sitemap_update_failed": "Failed to start sitemap regeneration.",
"sitemap_urls_per_file": "URLs per sitemap file",
"sitemap_urls_per_file_hint": "When URLs exceed this number, the sitemap is split into multiple files and an index file is created — minimum 1000, maximum 50000 (search engine limit)",
"sitemap_gzip": "Sitemap compression (gzip)",
"sitemap_gzip_desc": "Compresses split sitemap files to reduce transfer size. The index file is not compressed."
},
"security": {
"title": "Security Settings",
@@ -1732,7 +1736,9 @@
"geoip_manual_update": "Update now",
"geoip_updating": "Updating...",
"geoip_update_success": "GeoIP DB update completed.",
"geoip_update_failed": "GeoIP DB update failed."
"geoip_update_failed": "GeoIP DB update failed.",
"seo_sitemap_cache": "Sitemap cache",
"seo_sitemap_cache_desc": "Caches sitemap.xml."
},
"notification_definitions": {
"title": "Notification Settings",
@@ -2731,4 +2737,4 @@
"license_modal": "Core License",
"notification_delete_all_confirm_modal": "Delete All Notifications"
}
}
}
@@ -1427,9 +1427,13 @@
"sitemap_cache_ttl_hint": "비워두면 고급 설정의 Sitemap 캐시 시간을 따릅니다. 값을 넣으면 이 화면의 값이 우선합니다 — 최소 3600초 (1시간), 최대 604800초 (7일)",
"sitemap_last_updated_at": "마지막 생성",
"sitemap_manual_update": "지금 생성",
"sitemap_updating": "생성 중...",
"sitemap_update_success": "Sitemap 생성이 완료되었습니다.",
"sitemap_update_failed": "Sitemap 생성에 실패했습니다."
"sitemap_updating": "시작하는 중...",
"sitemap_update_success": "Sitemap 재생성을 시작했습니다. 완료까지 시간이 걸릴 수 있습니다.",
"sitemap_update_failed": "Sitemap 재생성을 시작하지 못했습니다.",
"sitemap_urls_per_file": "Sitemap 파일당 URL 수",
"sitemap_urls_per_file_hint": "URL 이 이 수를 넘으면 sitemap 파일이 여러 개로 나뉘고 색인 파일이 함께 만들어집니다 — 최소 1000개, 최대 50000개 (검색엔진 규격 상한)",
"sitemap_gzip": "Sitemap 압축 (gzip)",
"sitemap_gzip_desc": "나뉜 sitemap 파일을 압축해 전송량을 줄입니다. 색인 파일은 압축하지 않습니다."
},
"security": {
"title": "보안 설정",
@@ -1736,7 +1740,9 @@
"geoip_manual_update": "지금 업데이트",
"geoip_updating": "업데이트 중...",
"geoip_update_success": "GeoIP DB 업데이트가 완료되었습니다.",
"geoip_update_failed": "GeoIP DB 업데이트에 실패했습니다."
"geoip_update_failed": "GeoIP DB 업데이트에 실패했습니다.",
"seo_sitemap_cache": "Sitemap 캐시",
"seo_sitemap_cache_desc": "sitemap.xml 을 캐시합니다."
},
"notification_definitions": {
"title": "알림 설정",
@@ -2735,4 +2741,4 @@
"license_modal": "코어 라이선스",
"notification_delete_all_confirm_modal": "알림 전체 삭제 확인"
}
}
}
@@ -570,6 +570,124 @@
]
}
]
},
{
"id": "cache_seo_sitemap",
"type": "basic",
"name": "Div",
"props": {
"className": "flex-between gap-4"
},
"responsive": {
"portable": {
"props": {
"className": "row-stack py-2"
}
}
},
"children": [
{
"type": "basic",
"name": "Div",
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex-center"
},
"children": [
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-heading"
},
"text": "$t:admin.settings.advanced.seo_sitemap_cache"
},
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-red-500 ml-1"
},
"text": "*"
}
]
},
{
"type": "basic",
"name": "P",
"props": {
"className": "text-tertiary"
},
"text": "$t:admin.settings.advanced.seo_sitemap_cache_desc"
}
]
},
{
"type": "basic",
"name": "Div",
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex-center gap-2"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex-center"
},
"children": [
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-label whitespace-nowrap"
},
"text": "$t:admin.settings.advanced.cache_ttl"
},
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-red-500 ml-1"
},
"text": "*"
}
]
},
{
"type": "basic",
"name": "Input",
"props": {
"type": "number",
"name": "advanced.seo_sitemap_cache_ttl",
"placeholder": "86400",
"min": "3600",
"max": "604800",
"disabled": "{{_computed.isReadOnly}}",
"className": "input input-w-xs {{_local.errors?.['advanced.seo_sitemap_cache_ttl'] ? 'input-error' : ''}}"
}
}
]
},
{
"type": "basic",
"name": "Span",
"if": "{{_local.errors?.['advanced.seo_sitemap_cache_ttl']}}",
"props": {
"className": "form-error"
},
"text": "{{_local.errors?.['advanced.seo_sitemap_cache_ttl']?.[0] ?? ''}}"
}
]
}
]
}
]
}
@@ -963,6 +963,81 @@
}
]
},
{
"id": "field_sitemap_urls_per_file",
"type": "basic",
"name": "Div",
"children": [
{
"type": "basic",
"name": "Label",
"props": {
"className": "form-label"
},
"text": "$t:admin.settings.seo.sitemap_urls_per_file"
},
{
"type": "basic",
"name": "Input",
"props": {
"type": "number",
"name": "seo.sitemap_urls_per_file",
"min": 1000,
"max": 50000,
"placeholder": "50000",
"className": ""
}
},
{
"type": "basic",
"name": "P",
"props": {
"className": "form-hint"
},
"text": "$t:admin.settings.seo.sitemap_urls_per_file_hint"
}
]
},
{
"id": "field_sitemap_gzip",
"type": "basic",
"name": "Div",
"props": {
"className": "flex-between"
},
"children": [
{
"type": "basic",
"name": "Div",
"children": [
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-heading"
},
"text": "$t:admin.settings.seo.sitemap_gzip"
},
{
"type": "basic",
"name": "P",
"props": {
"className": "text-label-subtle"
},
"text": "$t:admin.settings.seo.sitemap_gzip_desc"
}
]
},
{
"type": "composite",
"name": "Toggle",
"props": {
"name": "seo.sitemap_gzip",
"size": "md"
}
}
]
},
{
"id": "sitemap_last_updated_block",
"type": "basic",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sirsoft-admin_basic",
"version": "1.0.3",
"version": "1.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sirsoft-admin_basic",
"version": "1.0.3",
"version": "1.0.4",
"license": "MIT",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -1,6 +1,6 @@
{
"name": "sirsoft-admin_basic",
"version": "1.0.3",
"version": "1.0.4",
"description": "Gnuboard7 Basic Admin Template Components",
"type": "module",
"main": "dist/components.js",
@@ -5,7 +5,7 @@
"ko": "Admin Basic",
"en": "Admin Basic"
},
"version": "1.0.3",
"version": "1.0.4",
"license": "MIT",
"description": {
"ko": "그누보드7 기본 관리자 템플릿",
@@ -3,6 +3,7 @@
namespace Tests\Feature\Api\Admin;
use App\Enums\ExtensionOwnerType;
use App\Jobs\GenerateSitemapJob;
use App\Models\Permission;
use App\Models\Role;
use App\Models\SeoCacheStat;
@@ -10,7 +11,9 @@ use App\Models\User;
use App\Seo\Contracts\SeoCacheManagerInterface;
use App\Seo\SitemapManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Queue;
use Mockery;
use Tests\TestCase;
@@ -389,76 +392,72 @@ class SeoCacheControllerTest extends TestCase
}
/**
* 정상 재생성 시 200 + last_updated_at 데이터 반환
* 재생성 요청 시 200 + 예약 상태와 직전 생성 시각을 반환
*/
public function test_regenerate_sitemap_returns_200_with_data(): void
public function test_regenerate_sitemap_returns_200_with_dispatched_status(): void
{
$mock = Mockery::mock(SitemapManager::class);
$mock->shouldReceive('regenerate')
->once()
->andReturn([
'success' => true,
'status' => 'updated',
'message' => 'Sitemap 생성이 완료되었습니다.',
'data' => [
'last_updated_at' => '2026-04-29T10:00:00+00:00',
'size_bytes' => 1234,
'ttl' => 86400,
],
]);
$this->app->instance(SitemapManager::class, $mock);
Queue::fake();
Config::set('g7_settings.core.seo.sitemap_enabled', true);
Config::set('g7_settings.core.seo.sitemap_last_updated_at', '2026-04-29T10:00:00+00:00');
$response = $this->withHeader('Authorization', 'Bearer '.$this->token)
->postJson('/api/admin/seo/sitemap/regenerate');
$response->assertStatus(200);
$this->assertTrue($response->json('success'));
$this->assertSame('dispatched', $response->json('data.status'));
$this->assertSame('2026-04-29T10:00:00+00:00', $response->json('data.last_updated_at'));
$this->assertSame(1234, $response->json('data.size_bytes'));
}
/**
* Sitemap 비활성 상태 시 400 반환
* 재생성 요청이 큐 잡을 1건만 디스패치하는지 확인
*/
public function test_regenerate_sitemap_dispatches_job(): void
{
Queue::fake();
Config::set('g7_settings.core.seo.sitemap_enabled', true);
$this->withHeader('Authorization', 'Bearer '.$this->token)
->postJson('/api/admin/seo/sitemap/regenerate')
->assertStatus(200);
Queue::assertPushed(GenerateSitemapJob::class, 1);
}
/**
* 재생성 요청이 요청 스레드에서 동기 생성하지 않는지 확인
*
* 이 회귀가 무너지면 1.4M 규모에서 관리자 워커가 OOM 으로 죽습니다.
*/
public function test_regenerate_sitemap_does_not_generate_inline(): void
{
Queue::fake();
Config::set('g7_settings.core.seo.sitemap_enabled', true);
$mock = Mockery::mock(SitemapManager::class);
$mock->shouldNotReceive('regenerate');
$mock->shouldReceive('getStatus')->andReturn(['last_updated_at' => null]);
$this->app->instance(SitemapManager::class, $mock);
$this->withHeader('Authorization', 'Bearer '.$this->token)
->postJson('/api/admin/seo/sitemap/regenerate')
->assertStatus(200);
}
/**
* Sitemap 비활성 상태 시 400 반환 + 잡 미디스패치
*/
public function test_regenerate_sitemap_returns_400_when_disabled(): void
{
$mock = Mockery::mock(SitemapManager::class);
$mock->shouldReceive('regenerate')
->once()
->andReturn([
'success' => false,
'status' => 'disabled',
'message' => 'Sitemap 생성이 비활성화되어 있습니다.',
]);
$this->app->instance(SitemapManager::class, $mock);
Queue::fake();
Config::set('g7_settings.core.seo.sitemap_enabled', false);
$response = $this->withHeader('Authorization', 'Bearer '.$this->token)
->postJson('/api/admin/seo/sitemap/regenerate');
$response->assertStatus(400);
$this->assertFalse($response->json('success'));
}
/**
* 내부 실패 시 500 반환
*/
public function test_regenerate_sitemap_returns_500_on_failure(): void
{
$mock = Mockery::mock(SitemapManager::class);
$mock->shouldReceive('regenerate')
->once()
->andReturn([
'success' => false,
'status' => 'failed',
'message' => 'Sitemap 생성에 실패했습니다: contributor crashed',
]);
$this->app->instance(SitemapManager::class, $mock);
$response = $this->withHeader('Authorization', 'Bearer '.$this->token)
->postJson('/api/admin/seo/sitemap/regenerate');
$response->assertStatus(500);
$this->assertFalse($response->json('success'));
Queue::assertNotPushed(GenerateSitemapJob::class);
}
protected function tearDown(): void
@@ -2,12 +2,14 @@
namespace Tests\Feature\Seo;
use App\Contracts\Repositories\ConfigRepositoryInterface;
use App\Enums\ExtensionOwnerType;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
/**
@@ -35,6 +37,20 @@ class SaveSettingsRequestSeoTest extends TestCase
$this->token = $this->admin->createToken('test-token')->plainTextToken;
}
/**
* 저장된 설정 카테고리를 디스크에서 다시 읽습니다.
*
* g7_core_settings() 는 부팅 시점에 로드된 Config 를 보므로, 같은 프로세스에서
* 저장 직후 값을 확인할 때는 저장소를 직접 읽어야 합니다.
*
* @param string $category 설정 카테고리
* @return array 저장된 설정 배열
*/
private function storedSettings(string $category): array
{
return app(ConfigRepositoryInterface::class)->getCategory($category);
}
/**
* 관리자 역할 및 권한을 가진 사용자를 생성합니다.
*
@@ -99,7 +115,7 @@ class SaveSettingsRequestSeoTest extends TestCase
*
* @param array $seoData SEO 필드 데이터
*/
private function postSeoSettings(array $seoData): \Illuminate\Testing\TestResponse
private function postSeoSettings(array $seoData): TestResponse
{
return $this->actingAs($this->admin)
->postJson('/api/admin/settings', [
@@ -385,4 +401,239 @@ class SaveSettingsRequestSeoTest extends TestCase
$response->assertStatus(422)
->assertJsonValidationErrors(['seo.generator_content']);
}
// ========================================
// sitemap 분할/압축/서빙 설정 검증 테스트
// ========================================
/**
* sitemap 분할/압축/서빙 설정이 저장되어 설정값에 반영되는지 확인
*/
public function test_sitemap_generation_settings_are_saved(): void
{
$response = $this->postSeoSettings([
'sitemap_urls_per_file' => 20000,
'sitemap_gzip' => true,
'sitemap_serve_stale_on_miss' => false,
'sitemap_max_urls_per_contributor' => 100000,
]);
$response->assertStatus(200);
$seo = $this->storedSettings('seo');
$this->assertSame(20000, $seo['sitemap_urls_per_file']);
$this->assertTrue((bool) $seo['sitemap_gzip']);
$this->assertFalse((bool) $seo['sitemap_serve_stale_on_miss']);
$this->assertSame(100000, $seo['sitemap_max_urls_per_contributor']);
}
/**
* 파일당 URL 수 상한(50000)을 넘기면 422 응답
*
* 50000 은 sitemaps.org 프로토콜 상한이므로 저장 자체를 막습니다.
*/
public function test_sitemap_urls_per_file_exceeding_protocol_limit_fails(): void
{
$response = $this->postSeoSettings([
'sitemap_urls_per_file' => 50001,
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['seo.sitemap_urls_per_file']);
}
/**
* 파일당 URL 수 하한(1000) 미만이면 422 응답
*/
public function test_sitemap_urls_per_file_below_minimum_fails(): void
{
$response = $this->postSeoSettings([
'sitemap_urls_per_file' => 999,
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['seo.sitemap_urls_per_file']);
}
/**
* 파일당 URL 수 경계값(1000/50000)은 통과하는지 확인
*/
public function test_sitemap_urls_per_file_boundary_values_pass(): void
{
$this->postSeoSettings(['sitemap_urls_per_file' => 1000])->assertStatus(200);
$this->postSeoSettings(['sitemap_urls_per_file' => 50000])->assertStatus(200);
}
/**
* 수집기당 최대 URL 수는 0(무제한)을 허용하고 음수는 거부하는지 확인
*/
public function test_sitemap_max_urls_per_contributor_allows_zero_and_rejects_negative(): void
{
$this->postSeoSettings(['sitemap_max_urls_per_contributor' => 0])->assertStatus(200);
$this->postSeoSettings(['sitemap_max_urls_per_contributor' => -1])
->assertStatus(422)
->assertJsonValidationErrors(['seo.sitemap_max_urls_per_contributor']);
}
/**
* gzip 이 boolean 이 아니면 422 응답
*/
public function test_sitemap_gzip_non_boolean_fails_validation(): void
{
$response = $this->postSeoSettings([
'sitemap_gzip' => 'yes-please',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['seo.sitemap_gzip']);
}
// ========================================
// 고급 탭 Sitemap 캐시 기준값 (D19 의 메인 값)
// ========================================
/**
* 고급 탭 설정 저장 요청을 전송합니다.
*
* @param array $advancedData 고급 필드 데이터
*/
private function postAdvancedSettings(array $advancedData): TestResponse
{
return $this->actingAs($this->admin)
->postJson('/api/admin/settings', [
'_tab' => 'advanced',
'advanced' => $advancedData,
]);
}
/**
* 고급 탭의 Sitemap 캐시 기준값이 저장되어 cache 카테고리에 반영되는지 확인
*/
public function test_advanced_sitemap_cache_ttl_is_saved_to_cache_category(): void
{
$response = $this->postAdvancedSettings([
'cache_enabled' => true,
'layout_cache_enabled' => true,
'layout_cache_ttl' => 3600,
'stats_cache_enabled' => true,
'stats_cache_ttl' => 3600,
'seo_cache_enabled' => true,
'seo_cache_ttl' => 3600,
'seo_sitemap_cache_ttl' => 43200,
'debug_mode' => false,
'sql_query_log' => false,
]);
$response->assertStatus(200);
$this->assertSame(43200, $this->storedSettings('cache')['seo_sitemap_ttl']);
}
/**
* 고급 탭 Sitemap 캐시 기준값이 기본값(86400)을 허용하는지 확인
*
* 형제 캐시 TTL 규칙(최대 14400)을 그대로 재사용하면 화면 기본값조차 저장할 수 없게 됩니다.
*/
public function test_advanced_sitemap_cache_ttl_accepts_default_value(): void
{
$this->postAdvancedSettings([
'cache_enabled' => true,
'layout_cache_enabled' => true,
'layout_cache_ttl' => 3600,
'stats_cache_enabled' => true,
'stats_cache_ttl' => 3600,
'seo_cache_enabled' => true,
'seo_cache_ttl' => 3600,
'seo_sitemap_cache_ttl' => 86400,
'debug_mode' => false,
'sql_query_log' => false,
])->assertStatus(200);
$this->assertSame(86400, $this->storedSettings('cache')['seo_sitemap_ttl']);
}
/**
* 고급 탭 Sitemap 캐시 기준값의 허용 범위를 벗어나면 422 응답
*/
public function test_advanced_sitemap_cache_ttl_out_of_range_fails(): void
{
$this->postAdvancedSettings(['seo_sitemap_cache_ttl' => 604801])
->assertStatus(422)
->assertJsonValidationErrors(['advanced.seo_sitemap_cache_ttl']);
$this->postAdvancedSettings(['seo_sitemap_cache_ttl' => 3599])
->assertStatus(422)
->assertJsonValidationErrors(['advanced.seo_sitemap_cache_ttl']);
}
/**
* 신규 키를 보내지 않는 기존 클라이언트의 고급 탭 저장이 계속 동작하는지 확인
*
* 형제 키처럼 required 로 두면 이전 화면/외부 클라이언트의 저장이 422 로 깨집니다.
*/
public function test_advanced_tab_saves_without_new_sitemap_cache_key(): void
{
$this->postAdvancedSettings([
'cache_enabled' => true,
'layout_cache_enabled' => true,
'layout_cache_ttl' => 3600,
'stats_cache_enabled' => true,
'stats_cache_ttl' => 3600,
'seo_cache_enabled' => true,
'seo_cache_ttl' => 3600,
'debug_mode' => false,
'sql_query_log' => false,
])->assertStatus(200);
}
/**
* 두 탭의 SEO 캐시 TTL 안내 문구가 각자의 실제 한도를 알려주는지 확인
*
* 두 탭이 같은 다국어 키를 공유하면 PHP 배열의 나중 정의가 이겨서
* 한쪽 탭이 반드시 틀린 한도를 안내하게 됩니다(ko/en 이 서로 반대로 틀어졌던 사례).
*/
public function test_seo_cache_ttl_messages_state_each_tabs_own_limit(): void
{
$seoTabErrors = $this->postSeoSettings(['cache_ttl' => 86401])
->assertStatus(422)
->json('errors');
$advancedTabErrors = $this->postAdvancedSettings(['seo_cache_ttl' => 14401])
->assertStatus(422)
->json('errors');
$this->assertStringContainsString('86400', $seoTabErrors['seo.cache_ttl'][0]);
$this->assertStringContainsString('14400', $advancedTabErrors['advanced.seo_cache_ttl'][0]);
}
// ========================================
// 캐시 오버라이드 칸 비우기 (D19)
// ========================================
/**
* 캐시 오버라이드 칸을 비워서 저장하면 null(미설정)로 정규화되는지 확인
*
* 폼에서 칸을 비우면 빈 문자열로 전송될 수 있는데, 이것이 null 로 정규화되지 않으면
* "미설정" 으로 판정되지 않아 고급 설정 값을 따르지 못하고 오버라이드가 계속 발동합니다.
*/
public function test_emptied_cache_override_fields_are_normalized_to_null(): void
{
$this->postSeoSettings([
'cache_ttl' => 3600,
'sitemap_cache_ttl' => 7200,
])->assertStatus(200);
$seo = $this->storedSettings('seo');
$this->assertSame(3600, $seo['cache_ttl']);
$this->assertSame(7200, $seo['sitemap_cache_ttl']);
$this->postSeoSettings([
'cache_ttl' => '',
'sitemap_cache_ttl' => '',
])->assertStatus(200);
$seo = $this->storedSettings('seo');
$this->assertNull($seo['cache_ttl']);
$this->assertNull($seo['sitemap_cache_ttl']);
}
}
+227 -119
View File
@@ -2,169 +2,277 @@
namespace Tests\Feature\Seo;
use App\Seo\SitemapGenerator;
use App\Extension\Storage\CoreStorageDriver;
use App\Jobs\GenerateSitemapJob;
use App\Seo\SitemapFileStore;
use App\Seo\SitemapManager;
use App\Seo\SitemapWriter;
use App\Seo\SitemapXmlRenderer;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Mockery;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
/**
* Sitemap XML 엔드포인트 기능 테스트
*
* GET /sitemap.xml 라우트가 올바른 XML 응답을 반환하고,
* 캐싱/비활성화 설정이 올바르게 동작하는지 검증합니다.
* 봇 요청 스레드에서 sitemap 을 생성하지 않고(대용량 OOM 원인),
* 비공개 디스크에 커밋된 세트를 스트리밍 서빙하는 계약을 검증합니다.
* 신선도가 만료되면 생성은 큐에 맡기고 기존 세트(stale)를 내보냅니다.
*/
class SitemapTest extends TestCase
{
private CoreStorageDriver $storage;
/**
* 테스트 종료 - Mockery 리소스를 정리합니다.
* 테스트 초기화 - 가짜 비공개 디스크와 활성화 설정을 준비합니다.
*/
protected function tearDown(): void
protected function setUp(): void
{
Mockery::close();
parent::tearDown();
parent::setUp();
Config::set('app.locale', 'ko');
Config::set('app.supported_locales', ['ko']);
Config::set('g7_settings.core.seo.sitemap_enabled', true);
Storage::fake('local');
$this->storage = new CoreStorageDriver('local');
$this->app->instance(SitemapFileStore::class, new SitemapFileStore($this->storage));
Cache::forget('g7:core:'.SitemapManager::META_CACHE_KEY);
}
/**
* sitemap.xml 요청이 application/xml Content-Type을 반환하는지 확인합니다.
* 커밋된 sitemap 세트를 디스크에 만듭니다.
*
* @param int $urlCount 생성할 URL 수
* @param int $urlsPerFile 파일당 URL 수
* @param bool $gzip gzip 압축 여부
* @return array 커밋 결과 메타
*/
public function test_sitemap_returns_xml_content_type(): void
private function commitSet(int $urlCount = 3, int $urlsPerFile = 50000, bool $gzip = false): array
{
// sitemap 활성화 설정
Config::set('g7_settings.core.seo.sitemap_enabled', true);
$writer = new SitemapWriter(
$this->storage,
SitemapXmlRenderer::fromConfig(),
$urlsPerFile,
$gzip,
);
// 캐시에 XML 미리 저장하여 Generator 호출 방지
$sampleXml = '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>';
Cache::put('g7:core:seo.sitemap', $sampleXml, 86400);
$writer->open();
for ($i = 1; $i <= $urlCount; $i++) {
$writer->addUrl(['loc' => url("/page/{$i}")]);
}
$writer->close();
return $writer->commit();
}
/**
* 메타 캐시를 신선한 상태로 만듭니다.
*
* @param array $meta 커밋 메타
*/
private function markFresh(array $meta): void
{
Cache::put('g7:core:'.SitemapManager::META_CACHE_KEY, $meta, 86400);
}
/**
* 응답 본문을 문자열로 수집합니다.
*
* @param TestResponse $response 테스트 응답
* @return string 응답 본문
*/
private function streamBody($response): string
{
ob_start();
$response->baseResponse->sendContent();
return (string) ob_get_clean();
}
/**
* 신선한 세트가 있으면 디스크의 sitemapindex 를 XML 로 서빙하는지 확인합니다.
*/
public function test_index_is_served_from_disk_when_fresh(): void
{
$meta = $this->commitSet();
$this->markFresh($meta);
$response = $this->get('/sitemap.xml');
$response->assertStatus(200);
$response->assertHeader('Content-Type', 'application/xml');
$body = $this->streamBody($response);
$this->assertStringContainsString('<sitemapindex', $body);
$this->assertStringContainsString('/sitemap-1.xml', $body);
}
/**
* sitemap_enabled가 false일 때 404를 반환하는지 확인합니다.
* 신선한 세트를 서빙할 때는 생성 잡을 디스패치하지 않는지 확인합니다.
*/
public function test_sitemap_returns_404_when_disabled(): void
public function test_index_does_not_dispatch_job_when_fresh(): void
{
Queue::fake();
$meta = $this->commitSet();
$this->markFresh($meta);
$this->get('/sitemap.xml')->assertStatus(200);
Queue::assertNotPushed(GenerateSitemapJob::class);
}
/**
* 메타 캐시가 만료되면 봇 요청 스레드에서 생성하지 않고 잡만 디스패치하는지 확인합니다.
*
* 이 회귀가 무너지면 1.4M 규모에서 봇 요청이 OOM/타임아웃을 유발합니다.
*/
public function test_index_dispatches_job_and_serves_stale_on_cache_miss(): void
{
Queue::fake();
$this->commitSet();
// 메타 캐시 없음 = 신선도 만료 상태
$response = $this->get('/sitemap.xml');
$response->assertStatus(200);
$this->assertStringContainsString('<sitemapindex', $this->streamBody($response));
Queue::assertPushed(GenerateSitemapJob::class, 1);
}
/**
* serve_stale_on_miss 가 false 면 만료 시 stale 을 내보내지 않고 503 을 반환하는지 확인합니다.
*/
public function test_index_returns_503_on_miss_when_stale_serving_disabled(): void
{
Queue::fake();
Config::set('g7_settings.core.seo.sitemap_serve_stale_on_miss', false);
$this->commitSet();
$response = $this->get('/sitemap.xml');
$response->assertStatus(503);
$response->assertHeader('Retry-After', '120');
Queue::assertPushed(GenerateSitemapJob::class, 1);
}
/**
* 커밋된 세트가 전혀 없으면 503 + Retry-After 를 반환하는지 확인합니다.
*/
public function test_index_returns_503_with_retry_after_when_no_set_exists(): void
{
Queue::fake();
$response = $this->get('/sitemap.xml');
$response->assertStatus(503);
$response->assertHeader('Retry-After', '120');
Queue::assertPushed(GenerateSitemapJob::class, 1);
}
/**
* 자식 sitemap 라우트가 디스크 파일을 스트리밍하는지 확인합니다.
*/
public function test_child_route_streams_child_sitemap(): void
{
$this->commitSet(urlCount: 3, urlsPerFile: 2);
$response = $this->get('/sitemap-1.xml');
$response->assertStatus(200);
$body = $this->streamBody($response);
$this->assertStringContainsString('<urlset', $body);
$this->assertStringContainsString('/page/1', $body);
}
/**
* 인덱스가 가리키는 모든 자식 경로가 실제로 서빙되는지 확인합니다.
*
* 인덱스의 <loc> 규칙(SitemapFileStore::childUrl)과 라우트 경로가 어긋나면
* 검색엔진에 404 를 가리키는 인덱스를 내보내게 됩니다.
*/
public function test_index_child_locs_are_all_routable(): void
{
$meta = $this->commitSet(urlCount: 5, urlsPerFile: 2);
$this->assertSame(3, $meta['child_count']);
for ($n = 1; $n <= $meta['child_count']; $n++) {
$this->get(SitemapFileStore::childUrl($n))->assertStatus(200);
}
}
/**
* 존재하지 않는 자식 번호는 404 를 반환하는지 확인합니다.
*/
public function test_child_route_returns_404_for_unknown_child(): void
{
$this->commitSet(urlCount: 1, urlsPerFile: 50000);
$this->get('/sitemap-99.xml')->assertStatus(404);
}
/**
* gzip 세트는 .xml.gz 경로에서 Content-Encoding 과 함께 서빙되는지 확인합니다.
*/
public function test_child_route_serves_gzip_set(): void
{
$this->commitSet(urlCount: 2, urlsPerFile: 50000, gzip: true);
$response = $this->get('/sitemap-1.xml.gz');
$response->assertStatus(200);
$response->assertHeader('Content-Encoding', 'gzip');
$this->assertStringContainsString('<urlset', (string) gzdecode($this->streamBody($response)));
}
/**
* sitemap_enabled 가 false 이면 인덱스/자식 모두 404 를 반환하는지 확인합니다.
*/
public function test_routes_return_404_when_disabled(): void
{
$meta = $this->commitSet();
$this->markFresh($meta);
Config::set('g7_settings.core.seo.sitemap_enabled', false);
$response = $this->get('/sitemap.xml');
$response->assertStatus(404);
$this->get('/sitemap.xml')->assertStatus(404);
$this->get('/sitemap-1.xml')->assertStatus(404);
}
/**
* 캐시에 저장된 XML이 있으면 해당 내용을 반환하는지 확인합니다.
* sitemap_enabled 가 null 이면 (bool) 캐스팅으로 비활성 처리되는지 확인합니다.
*/
public function test_sitemap_returns_cached_content(): void
{
Config::set('g7_settings.core.seo.sitemap_enabled', true);
$cachedXml = '<?xml version="1.0" encoding="UTF-8"?>'
.'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
.'<url><loc>https://example.com/cached</loc></url>'
.'</urlset>';
Cache::put('g7:core:seo.sitemap', $cachedXml, 86400);
$response = $this->get('/sitemap.xml');
$response->assertStatus(200);
$response->assertSee('https://example.com/cached');
$this->assertSame($cachedXml, $response->getContent());
}
/**
* 캐시가 없을 때 SitemapGenerator::generate()를 호출하고
* 결과를 캐시에 저장하는지 확인합니다.
*/
public function test_sitemap_generates_and_caches_when_not_cached(): void
{
Config::set('g7_settings.core.seo.sitemap_enabled', true);
Config::set('g7_settings.core.seo.sitemap_cache_ttl', 3600);
// 캐시 비우기
Cache::forget('g7:core:seo.sitemap');
$generatedXml = '<?xml version="1.0" encoding="UTF-8"?>'
.'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
.'<url><loc>https://example.com/generated</loc></url>'
.'</urlset>';
// SitemapGenerator Mock
$generatorMock = Mockery::mock(SitemapGenerator::class);
$generatorMock->shouldReceive('generate')
->once()
->andReturn($generatedXml);
// 컨테이너에 Mock 바인딩
$this->app->instance(SitemapGenerator::class, $generatorMock);
$response = $this->get('/sitemap.xml');
$response->assertStatus(200);
$response->assertHeader('Content-Type', 'application/xml');
$this->assertSame($generatedXml, $response->getContent());
// 캐시에 저장되었는지 확인
$this->assertSame($generatedXml, Cache::get('g7:core:seo.sitemap'));
}
/**
* sitemap_enabled가 null로 설정되면 (bool) 캐스팅에 의해 비활성으로 처리되는지 확인합니다.
*
* Config에 키가 null로 존재하면 Config::get()이 null을 반환하고,
* (bool) null = false이므로 404가 됩니다.
*/
public function test_sitemap_returns_404_when_enabled_is_null(): void
public function test_index_returns_404_when_enabled_is_null(): void
{
Config::set('g7_settings.core.seo.sitemap_enabled', null);
$response = $this->get('/sitemap.xml');
$response->assertStatus(404);
$this->get('/sitemap.xml')->assertStatus(404);
}
/**
* sitemap_enabled가 true일 때 정상 동작하는지 확인합니다.
* 서빙된 인덱스/자식이 유효한 XML 인지 확인합니다.
*/
public function test_sitemap_works_when_explicitly_enabled(): void
public function test_served_documents_are_valid_xml(): void
{
Config::set('g7_settings.core.seo.sitemap_enabled', true);
$meta = $this->commitSet(urlCount: 3, urlsPerFile: 2);
$this->markFresh($meta);
$sampleXml = '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>';
Cache::put('g7:core:seo.sitemap', $sampleXml, 86400);
$index = simplexml_load_string($this->streamBody($this->get('/sitemap.xml')));
$this->assertNotFalse($index, 'sitemapindex 응답이 유효한 XML 이어야 합니다');
$this->assertSame('sitemapindex', $index->getName());
$response = $this->get('/sitemap.xml');
$response->assertStatus(200);
}
/**
* sitemap XML 응답 본문이 XML 형식인지 확인합니다.
*/
public function test_sitemap_response_body_is_valid_xml(): void
{
Config::set('g7_settings.core.seo.sitemap_enabled', true);
$validXml = '<?xml version="1.0" encoding="UTF-8"?>'."\n"
.'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'."\n"
.' <url>'."\n"
.' <loc>https://example.com/</loc>'."\n"
.' </url>'."\n"
.'</urlset>';
Cache::put('g7:core:seo.sitemap', $validXml, 86400);
$response = $this->get('/sitemap.xml');
$response->assertStatus(200);
// XML 파싱이 성공하는지 확인
$xml = simplexml_load_string($response->getContent());
$this->assertNotFalse($xml, 'sitemap 응답이 유효한 XML이어야 합니다');
$this->assertSame('urlset', $xml->getName());
$child = simplexml_load_string($this->streamBody($this->get('/sitemap-1.xml')));
$this->assertNotFalse($child, '자식 sitemap 응답이 유효한 XML 이어야 합니다');
$this->assertSame('urlset', $child->getName());
}
}
@@ -0,0 +1,69 @@
<?php
namespace Tests\Feature\Settings;
use App\Contracts\Repositories\ConfigRepositoryInterface;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
/**
* settings 디스크 격리 회귀 테스트
*
* settings 디스크의 root 는 storage/app/settings — 개발/운영이 실제로 쓰는 설정 파일입니다.
* TestCase 가 이 디스크를 페이크로 대체하지 않으면, 설정 저장 경로를 타는 모든 테스트가
* 실제 설정을 덮어쓰고 RefreshDatabase 로도 되돌아가지 않습니다.
*
* 실제 피해 사례: 설정 저장 테스트가 general.json 의 사이트명/언어를 테스트 값으로 덮어써
* 개발 사이트 언어가 ja 로 바뀌어 있었고, SEO/캐시 TTL 도 테스트 값으로 대체되어 있었음.
*
* 이 가드가 풀리면 조용히 환경이 오염되므로(테스트는 계속 green) 명시적으로 고정합니다.
*/
class SettingsDiskIsolationTest extends TestCase
{
/**
* settings 디스크가 페이크로 대체되어 실경로를 가리키지 않는지 확인합니다.
*
* config 의 root 는 페이크 후에도 실경로 그대로이므로(Storage::fake 는 해석된 디스크
* 인스턴스만 교체), 반드시 해석된 디스크의 경로로 판정해야 합니다.
*/
public function test_settings_disk_is_faked_and_does_not_point_to_real_storage(): void
{
$root = str_replace('\\', '/', Storage::disk('settings')->path(''));
$this->assertStringContainsString(
'framework/testing',
$root,
'settings 디스크가 페이크로 대체되지 않았습니다 — 테스트가 실제 설정 파일을 덮어씁니다.'
);
$this->assertStringNotContainsString(
'storage/app/settings',
$root,
'settings 디스크가 실제 설정 경로를 가리킵니다.'
);
}
/**
* 설정을 저장해도 실제 설정 파일이 만들어지지 않는지 확인합니다.
*/
public function test_saving_settings_does_not_write_to_real_storage_path(): void
{
$realPath = storage_path('app/settings/general.json');
$before = file_exists($realPath) ? file_get_contents($realPath) : null;
app(ConfigRepositoryInterface::class)->saveCategory('general', [
'site_name' => '격리 검증용 값',
'language' => 'ja',
]);
$after = file_exists($realPath) ? file_get_contents($realPath) : null;
$this->assertSame($before, $after, '설정 저장이 실제 설정 파일을 변경했습니다.');
// 저장 자체는 페이크 디스크에서 정상 동작해야 한다 (격리가 기능을 죽이면 안 됨)
Storage::disk('settings')->assertExists('general.json');
$this->assertSame(
'격리 검증용 값',
app(ConfigRepositoryInterface::class)->getCategory('general')['site_name']
);
}
}
@@ -0,0 +1,177 @@
/**
* E2E: 환경설정 > SEO/고급 — Sitemap 생성 설정 칸 (#481)
*
* @scenario admin_settings_sitemap_generation_fields
* @effects fields_mounted, fields_persisted, regenerate_is_async
*
* 배경: 대용량 사이트에서 sitemap 을 여러 파일로 나눠 생성하도록 분할 기준/압축 설정을
* 추가하고, 관리자 "지금 생성" 버튼을 즉시 실행에서 큐 예약으로 바꿨다. 설정 칸이 실제로
* 마운트·저장되는지, 재생성 버튼이 응답을 기다리며 멈추지 않는지 브라우저에서 확인한다.
*
* 검증:
* 1. SEO 탭에 분할 기준/압축 칸이 번역문과 함께 마운트된다
* 2. 분할 기준을 바꿔 저장하면 422 없이 성공하고 재진입 시 유지된다
* 3. 고급 탭에 Sitemap 캐시 기준값 칸이 마운트되고 기본값(86400)이 저장된다
* 4. "지금 생성" 클릭이 즉시 응답을 받는다 (동기 생성이면 대용량에서 타임아웃)
*/
import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth';
const URLS_PER_FILE = '#field_sitemap_urls_per_file';
const GZIP_ROW = '#field_sitemap_gzip';
const ADVANCED_SITEMAP_CACHE = '#cache_seo_sitemap';
/** 관리자 환경설정 SEO 탭 진입 */
async function gotoSeoTab(page: import('@playwright/test').Page): Promise<void> {
await page.goto('/admin/settings?tab=seo');
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
await expect(page.locator(URLS_PER_FILE)).toBeAttached({ timeout: 20_000 });
}
/** 관리자 환경설정 고급 탭 진입 */
async function gotoAdvancedTab(page: import('@playwright/test').Page): Promise<void> {
await page.goto('/admin/settings?tab=advanced');
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
await expect(page.locator(ADVANCED_SITEMAP_CACHE)).toBeAttached({ timeout: 20_000 });
}
// @scenario tab=seo, permitted=yes
// @effects fields_mounted
test('@smoke #481 - SEO 탭에 Sitemap 분할 기준/압축 칸이 번역문과 함께 마운트된다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoSeoTab(page);
expect(page.url()).not.toMatch(/\/admin\/login/);
const urlsPerFile = page.locator(URLS_PER_FILE);
await expect(urlsPerFile).toBeAttached();
// 다국어 키 미해석 회귀 가드
const urlsText = (await urlsPerFile.innerText()).trim();
expect(urlsText).not.toContain('$t:');
expect(urlsText.length).toBeGreaterThan(0);
// 프로토콜 상한이 입력 단계에서 그대로 반영된다
const input = urlsPerFile.locator('input[type="number"]').first();
await expect(input).toHaveAttribute('max', '50000');
await expect(input).toHaveAttribute('min', '1000');
const gzipRow = page.locator(GZIP_ROW);
await expect(gzipRow).toBeAttached();
expect((await gzipRow.innerText()).trim()).not.toContain('$t:');
await expect(gzipRow.locator('input[type="checkbox"]').first()).toBeAttached();
});
// @scenario tab=seo, permitted=yes
// @effects fields_persisted
test('#481 - 분할 기준을 바꿔 저장하면 성공하고 재진입 시 유지된다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoSeoTab(page);
const input = page.locator(`${URLS_PER_FILE} input[type="number"]`).first();
// 폼이 저장된 설정으로 채워진 뒤에 수정해야 한다 (로드 전 입력은 다른 필드가 빈 채로 전송됨)
await expect.poll(() => input.inputValue(), { timeout: 15_000 }).not.toBe('');
const original = await input.inputValue();
const next = original === '30000' ? '40000' : '30000';
await input.fill(next);
await expect(page.locator('#save_button')).toBeEnabled({ timeout: 10_000 });
const save = page.waitForResponse(
(r) => r.url().includes('/api/admin/settings') && r.request().method() === 'POST',
{ timeout: 20_000 }
);
await page.locator('#save_button').click();
// 신규 키가 검증 규칙에 없으면 422 로 떨어진다 (배관 누락 회귀 가드)
expect((await save).status()).toBe(200);
await gotoSeoTab(page);
const reloaded = page.locator(`${URLS_PER_FILE} input[type="number"]`).first();
await expect.poll(() => reloaded.inputValue(), { timeout: 15_000 }).toBe(next);
// 원상 복구 (E2E 는 실제 환경 설정을 건드린다)
await reloaded.fill(original);
const restore = page.waitForResponse(
(r) => r.url().includes('/api/admin/settings') && r.request().method() === 'POST',
{ timeout: 20_000 }
);
await page.locator('#save_button').click();
expect((await restore).status()).toBe(200);
});
// @scenario tab=advanced, permitted=yes
// @effects fields_mounted, fields_persisted
test('#481 - 고급 탭 Sitemap 캐시 기준값 칸이 마운트되고 기본값이 저장된다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoAdvancedTab(page);
const row = page.locator(ADVANCED_SITEMAP_CACHE);
await expect(row).toBeAttached();
expect((await row.innerText()).trim()).not.toContain('$t:');
const input = row.locator('input[type="number"]').first();
await expect(input).toBeAttached();
// 폼이 저장된 설정으로 채워진 뒤에 수정해야 한다 (로드 전 입력은 다른 필드가 빈 채로 전송됨)
await expect.poll(() => input.inputValue(), { timeout: 15_000 }).not.toBe('');
const original = await input.inputValue();
// 기본값 86400 은 형제 칸 규칙(최대 14400)을 재사용하면 저장 자체가 막힌다
const next = original === '86400' ? '43200' : '86400';
await input.fill(next);
await expect(page.locator('#save_button')).toBeEnabled({ timeout: 10_000 });
const save = page.waitForResponse(
(r) => r.url().includes('/api/admin/settings') && r.request().method() === 'POST',
{ timeout: 20_000 }
);
await page.locator('#save_button').click();
expect((await save).status()).toBe(200);
// 원상 복구 (E2E 는 실제 환경 설정을 건드린다)
await gotoAdvancedTab(page);
const restored = page.locator(`${ADVANCED_SITEMAP_CACHE} input[type="number"]`).first();
await expect.poll(() => restored.inputValue(), { timeout: 15_000 }).toBe(next);
await restored.fill(original);
const restore = page.waitForResponse(
(r) => r.url().includes('/api/admin/settings') && r.request().method() === 'POST',
{ timeout: 20_000 }
);
await page.locator('#save_button').click();
expect((await restore).status()).toBe(200);
});
// @scenario tab=seo, permitted=yes
// @effects regenerate_is_async
test('#481 - "지금 생성" 클릭이 큐에 예약되고 즉시 응답한다', async ({ page }) => {
const token = issueToken('core.settings.read', 'core.settings.update');
await authenticatePage(page, token);
await gotoSeoTab(page);
const regenerate = page.waitForResponse(
(r) => r.url().includes('/api/admin/seo/sitemap/regenerate'),
{ timeout: 20_000 }
);
const startedAt = Date.now();
await page.locator('#sitemap_last_updated_block button').first().click();
const response = await regenerate;
const elapsed = Date.now() - startedAt;
expect(response.status()).toBe(200);
expect((await response.json())?.data?.status).toBe('dispatched');
// 동기 생성으로 되돌아가면 응답 시간이 데이터 규모에 비례해 늘어난다.
// 절대 하한 대신 "예약 응답이라면 당연히 만족하는" 상한으로 회귀만 잡는다.
expect(elapsed).toBeLessThan(10_000);
});
+27
View File
@@ -6,6 +6,7 @@ use App\Extension\Testing\ExtensionTestAllowlist;
use App\Listeners\Identity\EnforceIdentityPolicyListener;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
abstract class TestCase extends BaseTestCase
{
@@ -39,12 +40,18 @@ abstract class TestCase extends BaseTestCase
* createApplication() 단계에서 모든 ServiceProvider 의 register()/boot() 가
* 실행되므로, allowlist 는 그 이전(= parent::setUp() 호출 전)에 설정되어야
* provider 자동 등록 가드가 올바른 시점에 적용됩니다.
*
* settings 디스크는 앱 부팅 직후 페이크로 대체합니다 (아래 fakeSettingsDisk).
*/
protected function setUp(): void
{
// 앱 생성 전에 확장 allowlist 설정 (provider register 가드 적용 시점 보장)
ExtensionTestAllowlist::set($this->resolveAllowedExtensions());
$this->afterApplicationCreated(function () {
$this->fakeSettingsDisk();
});
if (! self::$staleConnectionsCleaned) {
// Laravel 앱 부팅 전이므로 부트스트랩 후 콜백으로 등록
$this->afterApplicationCreated(function () {
@@ -56,6 +63,26 @@ abstract class TestCase extends BaseTestCase
parent::setUp();
}
/**
* settings 디스크를 페이크로 대체하여 실제 설정 파일을 보호합니다.
*
* `settings` 디스크의 root 는 `storage/app/settings` 로, 개발/운영 환경이 실제로
* 사용하는 설정 파일 그 자체입니다. 페이크로 대체하지 않으면 설정 저장 경로를 타는
* 테스트(설정 API 호출, ConfigRepository::saveCategory 등)가 실제 설정을 덮어쓰고,
* RefreshDatabase 는 DB 만 되돌리므로 그 오염이 그대로 남습니다.
*
* 실제 피해 사례: 설정 저장 테스트가 `general.json` 의 사이트명/언어를 테스트 값으로
* 덮어써 개발 사이트 언어가 ja 로 바뀌어 있었고, SEO/캐시 TTL 도 테스트 값으로 대체됨.
*
* 설정 *읽기* 는 부팅 시 Config(`g7_settings.*`)로 이미 적재된 뒤이므로 이 페이크의
* 영향을 받지 않습니다. 저장소를 직접 읽어야 하는 테스트는 페이크 디스크에 직접
* 값을 넣고 검증합니다.
*/
private function fakeSettingsDisk(): void
{
Storage::fake('settings');
}
/**
* 테스트 종료 시 확장 allowlist 를 초기화합니다.
*
+61 -8
View File
@@ -4,7 +4,9 @@ namespace Tests\Unit\Jobs;
use App\Jobs\GenerateSitemapJob;
use App\Seo\SitemapManager;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Mockery;
use Tests\TestCase;
@@ -28,20 +30,71 @@ class GenerateSitemapJobTest extends TestCase
*/
public function test_job_implements_should_queue(): void
{
$job = new GenerateSitemapJob();
$job = new GenerateSitemapJob;
$this->assertInstanceOf(ShouldQueue::class, $job);
}
/**
* tries=3, timeout=300 속성을 확인합니다.
* 대용량 생성을 견디는 큐 속성을 확인합니다.
*/
public function test_job_has_correct_tries_and_timeout(): void
{
$job = new GenerateSitemapJob();
$job = new GenerateSitemapJob;
$this->assertSame(3, $job->tries);
$this->assertSame(300, $job->timeout);
$this->assertSame(1800, $job->timeout);
$this->assertSame(120, $job->backoff);
}
/**
* ShouldBeUnique 를 구현하여 동시 실행을 막는지 확인합니다.
*
* 봇 요청 캐시 미스마다 dispatch 되어도 큐에 한 건만 남아야 합니다.
*/
public function test_job_implements_should_be_unique(): void
{
$job = new GenerateSitemapJob;
$this->assertInstanceOf(ShouldBeUnique::class, $job);
}
/**
* 유니크 락 키가 인스턴스와 무관하게 고정값인지 확인합니다.
*
* 인스턴스마다 키가 다르면 중복 디스패치가 그대로 통과합니다.
*/
public function test_unique_id_is_stable_across_instances(): void
{
$this->assertSame('seo-sitemap', (new GenerateSitemapJob)->uniqueId());
$this->assertSame('seo-sitemap', (new GenerateSitemapJob)->uniqueId());
}
/**
* 유니크 락이 영구 고착되지 않도록 만료 시간이 설정되어 있는지 확인합니다.
*/
public function test_unique_lock_expires(): void
{
$job = new GenerateSitemapJob;
$this->assertSame(900, $job->uniqueFor);
}
/**
* retryUntil 이 재시도 누적을 30분으로 제한하는지 확인합니다.
*/
public function test_retry_until_limits_retry_window(): void
{
Carbon::setTestNow(Carbon::parse('2026-07-17 10:00:00'));
$job = new GenerateSitemapJob;
$this->assertSame(
'2026-07-17 10:30:00',
Carbon::instance($job->retryUntil())->format('Y-m-d H:i:s')
);
Carbon::setTestNow();
}
/**
@@ -68,7 +121,7 @@ class GenerateSitemapJobTest extends TestCase
return $context['size'] === 1234 && $context['ttl'] === 86400;
}));
$job = new GenerateSitemapJob();
$job = new GenerateSitemapJob;
$job->handle($manager);
$this->addToAssertionCount(Mockery::getContainer()->mockery_getExpectationCount());
@@ -92,7 +145,7 @@ class GenerateSitemapJobTest extends TestCase
->once()
->with('[SEO] Sitemap generation skipped (disabled)');
$job = new GenerateSitemapJob();
$job = new GenerateSitemapJob;
$job->handle($manager);
$this->addToAssertionCount(Mockery::getContainer()->mockery_getExpectationCount());
@@ -115,7 +168,7 @@ class GenerateSitemapJobTest extends TestCase
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('contributor crashed');
$job = new GenerateSitemapJob();
$job = new GenerateSitemapJob;
$job->handle($manager);
}
@@ -132,7 +185,7 @@ class GenerateSitemapJobTest extends TestCase
return $context['error'] === 'Sitemap generation timeout';
}));
$job = new GenerateSitemapJob();
$job = new GenerateSitemapJob;
$job->failed($exception);
$this->addToAssertionCount(Mockery::getContainer()->mockery_getExpectationCount());