7.0.9 릴리즈 사전 점검에서 공개 배포물에 실릴 결함을 전수 정리한다. 출시일자: 이번 사이클에 버전이 오르거나 새로 추가된 대상 10건의 CHANGELOG 출시일을 실제 공개일로 정렬. 결제 3종은 직전 릴리즈 페이로드로 이미 나갔으므로 제외했다. 내부 역할 호칭: 공개 배포물 26파일 49곳을 중립 표현으로 치환. 이미 공개된 페이로드에는 실질 0건이라 이번 릴리즈가 첫 유입이었다. 최초 출시 규격: 신규 플러그인과 그 언어팩의 CHANGELOG 를 선례 규격으로 재구성. 작성 중 확인되지 않은 서술 2건은 코드 실측으로 교정했다. 번들 ja 언어팩: 번역 값에 남은 한글 40건 정정. 같은 용어가 세 갈래로 갈려 있던 표기도 함께 통일했다. 정정값의 근거는 저장소가 이미 갖고 있었다 — 테스트가 정답으로 단언하는 값과 배포물이 어긋난 상태였다. 재발 방지: 원인은 번역 생성기의 용어집 미등록이므로 출력만 고치면 다음 빌드에서 되살아난다. 용어집에 8건을 등록하고, 규정 3건과 잔존 검출 룰을 신설했다. 룰은 수정 전 배포물로 red 가 되는지를 픽스처로 고정해 판정기가 모집단에 닿는지를 증명한다.
620 lines
40 KiB
Markdown
620 lines
40 KiB
Markdown
# 언어팩 시스템 (Language Packs)
|
||
|
||
## TL;DR (5초 요약)
|
||
|
||
```text
|
||
1. 코어/번들 확장의 lang/{ko,en}/ 는 가상 보호 행으로 자동 노출 (DB 없이 항상 active+protected, 수정 불가)
|
||
2. lang-packs/_bundled/ 패키지(ja, fr 등)는 비보호 — 사용자가 install/uninstall/update/activate/deactivate 자유
|
||
3. 활성화 시 의존성 + 버전 호환성 자동 검사 (호스트 확장 active + requires.target_version 제약)
|
||
4. 호스트 확장 비활성화 시 종속 언어팩 자동 비활성화. 재활성화 시 모달로 "다음 언어팩도 활성화 하시겠습니까" 질의
|
||
5. 업데이트 우선순위: GitHub 1순위 + bundled 폴백 (force 시 bundled 우선) — 모듈/플러그인 패턴 동일
|
||
```
|
||
|
||
## 개요
|
||
|
||
G7 의 언어팩 시스템은 모듈/플러그인/템플릿 시스템과 동일한 멘탈 모델을 재사용한 **설치 가능한 다국어 아티팩트** 입니다. `lang-packs/` 디렉토리가 쓰기 가능 영역으로 분리되어 있어 코어 업데이트 시에도 언어팩이 유실되지 않습니다.
|
||
|
||
## 디렉토리 구조
|
||
|
||
```
|
||
lang-packs/
|
||
├── _bundled/ # Git 추적 (번들 메타데이터)
|
||
│ ├── g7-core-ko/
|
||
│ │ └── language-pack.json
|
||
│ ├── g7-core-en/
|
||
│ ├── g7-template-sirsoft-admin_basic-ko/
|
||
│ ├── g7-template-sirsoft-admin_basic-en/
|
||
│ ├── g7-template-sirsoft-basic-ko/
|
||
│ └── g7-template-sirsoft-basic-en/
|
||
├── _pending/ # Git 제외, 업로드 임시
|
||
└── {identifier}/ # Git 제외, 활성 설치본
|
||
├── language-pack.json
|
||
├── backend/{locale}/*.php # Laravel trans 파일
|
||
├── frontend/*.json # 템플릿 다국어 fragment
|
||
└── seed/*.json # 시드 다국어 데이터
|
||
```
|
||
|
||
## 코어 다국어 자원의 위치
|
||
|
||
코어 자체의 다국어 자원은 모듈/플러그인/템플릿과 동일한 `lang/` 트리 구조를
|
||
사용한다 (언어팩 디렉토리 구조와는 별개). 어떤 템플릿이 부팅되든 자동 노출된다.
|
||
|
||
```text
|
||
lang/
|
||
├── ko/ # 백엔드 .php (Laravel __() / trans())
|
||
│ └── activity_log.php, admin_layout.php, auth.php, ...
|
||
├── en/ # 백엔드 .php
|
||
│ └── activity_log.php, admin_layout.php, ...
|
||
├── partial/ # 프론트엔드 .json 분할 (선택)
|
||
│ ├── ko/
|
||
│ │ └── (영역별 .json — 필요 시)
|
||
│ └── en/
|
||
├── ko.json # 프론트엔드 엔트리 ($t: 프리픽스)
|
||
└── en.json # 프론트엔드 엔트리
|
||
```
|
||
|
||
| 축 | 코어 | 모듈 (sirsoft-board) | 동일 패턴 |
|
||
|---|---|---|---|
|
||
| 백엔드 .php | `lang/{ko,en}/*.php` | `modules/_bundled/sirsoft-board/resources/lang/{ko,en}/*.php` | ✓ |
|
||
| 프론트엔드 엔트리 | `lang/{ko,en}.json` | `modules/_bundled/sirsoft-board/resources/lang/{ko,en}.json` | ✓ |
|
||
| 프론트엔드 partial | `lang/partial/{ko,en}/*.json` | `modules/_bundled/sirsoft-board/resources/lang/partial/{ko,en}/*.json` | ✓ |
|
||
|
||
### 코어 키 공간 컨벤션
|
||
|
||
코어 프론트엔드 lang JSON 의 root 키는 `core.*` prefix 로 일원화한다. 영역별 세분화:
|
||
|
||
- `core.errors.*` — 템플릿 엔진 에러 메시지 (`TemplateNotFoundError` 등)
|
||
- `core.toast.*` — 코어가 발화하는 시스템 토스트
|
||
- `core.layout_editor.*` — 레이아웃 편집기 chrome
|
||
- `core.<영역>.*` — 향후 코어 UI 추가 시
|
||
|
||
호스팅 템플릿/모듈/플러그인의 lang JSON 은 root 에 `core` 키를 정의하지 않는다.
|
||
정적 검사가 차단한다.
|
||
|
||
### 런타임 병합 흐름
|
||
|
||
`TemplateService::getLanguageDataWithModules` 가 다음 순서로 병합:
|
||
|
||
1. 코어 자체 (`lang/{locale}.json` + partial) — 가장 베이스
|
||
2. 템플릿 (`templates/{id}/lang/{locale}.json`)
|
||
3. 모듈 (`modules/{id}/resources/lang/{locale}.json`) — 식별자 wrap
|
||
4. 플러그인 (`plugins/{id}/lang/{locale}.json`) — 식별자 wrap
|
||
5. 활성 언어팩 (`lang-packs/{id}/frontend/*.json` — `MergeFrontendLanguage` 필터)
|
||
|
||
코어가 베이스로 들어가 가장 낮은 우선순위 — 템플릿/모듈/플러그인/언어팩이 코어 키를
|
||
덮어쓸 수 있다 (예: `g7-core-ja/frontend/ko.json` 이 활성화되면 일본어로 번역).
|
||
|
||
### 병합 정책 — Deep Merge (재귀 병합)
|
||
|
||
`TemplateService::getLanguageDataWithModules` 는 `array_merge` 가 아닌 재귀 deep merge
|
||
(`deepMergeLanguageData`) 로 4개 레이어를 합친다. 동일 top-level 키(예: `layout_editor`,
|
||
`core`, `auth`) 의 하위 트리를 leaf 까지 내려가며 합치고, 어느 한쪽이 다른 레이어의
|
||
부분 트리만 정의해도 나머지 형제 키가 보존된다.
|
||
|
||
- assoc 트리끼리 충돌 → 재귀 병합 (양쪽 leaf 모두 보존, 동일 leaf 는 뒤가 우선)
|
||
- list / scalar / 한쪽만 array → 뒤 입력으로 덮어쓰기
|
||
- 우선순위(낮음 → 높음): 코어 < 템플릿 < 모듈 < 플러그인 < 활성 언어팩(filter 훅)
|
||
|
||
이 정책으로 템플릿이 `layout_editor.palette` 만 정의해도 코어의 `layout_editor.chrome /
|
||
device / zoom / preview / save` 가 살아남는다. 과거 shallow `array_merge` 는 동일
|
||
top-level 키 시 트리 전체가 교체되어, 템플릿이 한 sub-key 만 정의해도 코어의 다른
|
||
sub-key 가 통째 누락되는 결함이 발생했다.
|
||
|
||
확장 작성 시 권장 패턴:
|
||
|
||
- 코어가 정의한 도메인 네임스페이스(예: `layout_editor.*`, `core.*`, `auth.*`) 안에
|
||
자신의 sub-key 만 정의하면 deep merge 가 자동으로 양쪽 보존
|
||
- 코어 leaf 를 의도적으로 오버라이드하려면 동일 키 경로에 leaf 만 정의 (트리 교체
|
||
의도가 아니라 leaf override 의도임을 코드로 표현)
|
||
- 모듈/플러그인은 식별자 wrap (`module.{id}.*`, `plugin.{id}.*`) 사용을 우선 — 코어
|
||
도메인과 충돌할 일이 없고, deep merge 의 잠재적 부작용도 회피
|
||
|
||
### 호스팅 템플릿 무관성
|
||
|
||
코어가 자체 자원을 베이스로 제공하므로, 호스팅 템플릿이 바뀌어도 `$t:core.errors.*`
|
||
같은 키는 항상 해석된다. 코어가 발화하는 에러/토스트/편집기 등이 호스팅 템플릿
|
||
종속이 되지 않는다.
|
||
|
||
## Manifest 명세 (language-pack.json)
|
||
|
||
언어팩 매니페스트는 모듈/플러그인/템플릿 매니페스트와 동일한 필드 구조로 정렬되어 있어 외부 작성자가 다른 확장과 동일한 표준으로 언어팩을 만들 수 있습니다.
|
||
|
||
```json
|
||
{
|
||
"identifier": "g7-core-ja",
|
||
"namespace": "g7",
|
||
"vendor": "sirsoft",
|
||
"name": {
|
||
"ko": "G7 코어 일본어 언어팩",
|
||
"en": "G7 core Japanese language pack",
|
||
"ja": "G7 コア 日本語 言語パック"
|
||
},
|
||
"description": {
|
||
"ko": "G7 코어 일본어 언어팩 (번들)",
|
||
"en": "G7 core Japanese language pack (bundled)",
|
||
"ja": "G7 コア 日本語 言語パック(バンドル)"
|
||
},
|
||
"version": "1.0.0",
|
||
"license": "MIT",
|
||
"scope": "core",
|
||
"target_identifier": null,
|
||
"locale": "ja",
|
||
"locale_name": "Japanese",
|
||
"locale_native_name": "日本語",
|
||
"text_direction": "ltr",
|
||
"g7_version": ">=7.0.0",
|
||
"requires": {
|
||
"target_version": null,
|
||
"depends_on_core_locale": false
|
||
},
|
||
"github_url": "",
|
||
"github_changelog_url": ""
|
||
}
|
||
```
|
||
|
||
### 필드
|
||
|
||
| 필드 | 필수 | 설명 |
|
||
|---|---|---|
|
||
| `identifier` | ✓ | 전역 고유 — 네이밍 공식: `{vendor-or-namespace}-{scope}-{target?}-{locale}` |
|
||
| `namespace` | ✓ | 식별자 prefix (예: `g7`, `acme`). `identifier` 의 prefix 와 일치해야 함 |
|
||
| `vendor` | ✓ | 제작자 식별자 (kebab-case). `namespace` 와는 의미 분리 — 동일 namespace 의 패키지를 여러 벤더가 발행 가능 |
|
||
| `name` | ✓ | 다국어 객체 — 최소 `ko`/`en` 키 + 자체 로케일 키. 카드 UI 표시명 |
|
||
| `description` | ✓ | 다국어 객체 — 동일 키 규칙. 상세 모달 설명 |
|
||
| `version` | ✓ | semver. 번들 패키지는 패치 단위 bump 후 CHANGELOG 항목 작성 |
|
||
| `license` | ✓ | SPDX 식별자 (예: `MIT`) |
|
||
| `scope` | ✓ | `core` / `module` / `plugin` / `template` |
|
||
| `target_identifier` | ✓ | scope=core 면 `null`, 그 외 호스트 확장 식별자 |
|
||
| `locale` | ✓ | IETF BCP-47 (예: `ja`, `zh-CN`, `pt-BR`) |
|
||
| `locale_name` | ✓ | 영문 언어명 (예: `Japanese`, `Simplified Chinese`) |
|
||
| `locale_native_name` | ✓ | 언어 선택 UI 표시명 (예: `日本語`) |
|
||
| `text_direction` | ✓ | `ltr` / `rtl` |
|
||
| `g7_version` | ✓ | 코어 호환 버전 제약 (top-level — 모듈/플러그인 매니페스트와 일관). 모듈 매니페스트의 `g7_version` 과 동일 의미 |
|
||
| `requires.target_version` | ✓ | scope=module/plugin/template 일 때 호스트 확장 버전 제약. scope=core 면 `null` |
|
||
| `requires.depends_on_core_locale` | ✓ | true 일 때 활성화 시 동일 locale 의 코어 언어팩 활성 상태를 강제 |
|
||
| `github_url` | ✓ | GitHub 저장소 URL (없으면 빈 문자열). 비어있지 않으면 GitHub 기반 업데이트 경로 활성화 — 모듈/플러그인 매니페스트와 동일 동작 |
|
||
| `github_changelog_url` | ✓ | GitHub release/CHANGELOG URL (없으면 빈 문자열) |
|
||
|
||
### 콘텐츠 파일
|
||
|
||
매니페스트의 `contents` 필드는 사용하지 않습니다. 패키지 디렉토리 (`lang-packs/{identifier}/`) 의 `backend/{locale}/`, `frontend/`, `seed/` 하위 파일이 자동 발견됩니다.
|
||
|
||
- `backend/{locale}/*.php` — Laravel `trans()` 파일 (코어/모듈/플러그인 PHP 다국어)
|
||
- `frontend/{locale}.json` — 템플릿 풀 페이로드
|
||
- `frontend/partial/{locale}/*.json` — 템플릿/모듈 partial 페이로드
|
||
- `seed/{entity}.json` — 다국어 시드 (NotificationDefinition, IdentityMessageDefinition 등)
|
||
|
||
### 들여쓰기 규칙
|
||
|
||
- `language-pack.json` (매니페스트) — **4-space** (모듈/플러그인/템플릿 매니페스트와 일관)
|
||
- 콘텐츠 JSON (`frontend/*.json`, `seed/*.json`) — 2-space
|
||
|
||
## 슬롯과 다중 벤더
|
||
|
||
**슬롯** = `(scope, target_identifier, locale)` 튜플. 동일 슬롯에 여러 벤더가 공존할 수 있으나 **active 는 1개만 허용** (`language_packs_slot_active_unique` functional index).
|
||
|
||
```
|
||
slot (core, null, ja):
|
||
- sirsoft-core-ja (active, vendor=sirsoft)
|
||
- acme-core-ja (installed, vendor=acme)
|
||
```
|
||
|
||
UI 에서 라디오로 즉시 전환 가능. 활성 팩이 제거되면 slot 의 다음 후보가 자동 승격됩니다.
|
||
|
||
## 설치 흐름
|
||
|
||
1. ZIP/GitHub/URL → `_pending/{tmp}/` 추출
|
||
2. `LanguagePackManifestValidator` → manifest 구조 검증
|
||
3. 보안 검사: `backend/` 외 PHP 파일 차단, `eval`/`include` 패턴 차단
|
||
4. 의존성 검사: `depends_on_core_locale=true` 면 코어 언어팩 active 여부 확인
|
||
5. 대상 확장 존재 확인 (modules/plugins/templates 테이블)
|
||
6. 다운그레이드 차단 (version_compare)
|
||
7. `_pending` → `lang-packs/{identifier}/` 이동
|
||
8. DB 레코드 생성/업데이트, 슬롯이 비어있으면 자동 active 승격
|
||
9. `HookManager::doAction('core.language_packs.after_activate', $pack)` → `SyncDatabaseTranslations` 리스너가 DB JSON 컬럼 갱신
|
||
|
||
### 웹 인스톨러 (`public/install/`) 동반 설치
|
||
|
||
신규 사이트 초기 설치 시 4단계 확장 선택 화면에서 번들 언어팩(`lang-packs/_bundled/*`)을 함께 선택할 수 있습니다.
|
||
|
||
- 인스톨러는 Laravel 부팅 전 단계로 동작하므로 `lang-packs/_bundled/*/language-pack.json` 매니페스트를 직접 스캔하여 카드를 노출합니다.
|
||
- 카드는 `locale` 별 서브헤딩으로 그룹핑되며, `scope` 가 `module`/`plugin`/`template` 인 항목은 종속 확장(`target_identifier`)이 함께 선택되어야 활성화됩니다. 종속 확장을 해제하면 그 언어팩 카드는 자동으로 비활성화 + 선택 해제됩니다.
|
||
- 코어 언어팩(`scope: "core"`)은 `target_identifier` 가 없으므로 항상 활성화 상태입니다.
|
||
- default 선택 정책은 모두 OFF — 사용자가 명시 선택. (관리자 설치 모달의 "모든 후보 ON" 정책과 차별화하여, 신규 사이트가 불필요한 언어팩으로 시작하지 않도록 함.)
|
||
- 설치 실행 시 5단계의 모든 확장 install/activate 가 완료된 뒤 `php artisan language-pack:install {identifier} --source=bundled` 가 선택된 각 언어팩에 대해 호출됩니다(자동 활성화 default).
|
||
- 언어팩 1건 설치 실패는 best-effort 처리 — 전체 설치를 중단하지 않고 경고 로그만 남긴 뒤 다음 언어팩으로 진행합니다. 코어/모듈/플러그인 install 실패와 달리 rollback 을 발생시키지 않습니다.
|
||
|
||
## 프로비저닝과 드리프트 발견 (supported_locales)
|
||
|
||
코어의 `ko`/`en` 은 가상 보호 행으로 DB 설치 없이 항상 서빙되지만, 그 외 로케일(예: `ja`)의 번들 언어팩은 `lang-packs/_bundled/` 에 소스가 있어도 설치본 디렉토리로 복사·등록되어야 서빙됩니다. 이 비대칭 때문에 "설치본이 없거나 어긋난" 상태가 오류 없이 조용히 `ko` 로 폴백할 수 있어, 다음 도구로 프로비저닝·발견을 지원합니다.
|
||
|
||
### `language-pack:provision` (멱등 프로비저닝)
|
||
|
||
```bash
|
||
php artisan language-pack:provision # supported_locales 의 비-base 로케일을 대상으로 미설치 번들 팩 설치
|
||
php artisan language-pack:provision --locale=ja # 특정 로케일만
|
||
php artisan language-pack:provision --scope=core # 스코프 한정
|
||
```
|
||
|
||
- 대상 로케일 기본값은 `config('app.supported_locales')` 에서 base locale(ko/en)을 뺀 집합입니다. 즉 "사이트가 쓰겠다고 선언한 로케일" 만 채웁니다(불필요한 로케일 대량 설치 없음).
|
||
- 미설치 번들 팩(신규 설치)과 설치본 파일이 사라진 드리프트 팩(복구)을 함께 대상으로 삼습니다. 정상 설치된 팩은 어느 쪽에도 해당하지 않아 재실행해도 신규 설치가 0 건으로 수렴합니다(완전 멱등). fresh install · 복구 · 시더가 공유하는 단일 프로비저닝 경로입니다.
|
||
- 대상 확장 미설치/미활성 등 설치 차단 사유가 있는 팩은 건너뛰고 경고만 남깁니다(best-effort).
|
||
|
||
### 미설치·드리프트 발견
|
||
|
||
- `language-pack:list` 는 설치된 DB 행뿐 아니라 "번들에 있으나 미설치"(`uninstalled`) 및 "active 로 기록됐으나 설치본 파일 부재(드리프트)" 를 함께 표시합니다. 드리프트 행의 Status 는 `active (파일 없음)` 처럼 표기됩니다.
|
||
- 관리자 언어팩 목록 화면도 동일하게 드리프트 행에 "파일 없음" 배지와 원클릭 재설치 버튼을 노출합니다(번들 소스 재설치).
|
||
- 재설치 버튼은 **번들 소스가 실재하는 팩**에만 뜹니다. 설치 경로(zip/GitHub/URL)와 무관하게 동일 식별자의 `lang-packs/_bundled/` 소스가 있으면 복구할 수 있고, 소스가 없는 서드파티 팩은 배지로 발견만 되고 복구 버튼은 뜨지 않습니다(복구할 원본이 없기 때문).
|
||
|
||
### supported_locales ↔ 번들 소스 정합
|
||
|
||
`config/app.php` 의 `supported_locales` 에 비-base 로케일을 선언했다면, 대응하는 코어 번들 소스(`lang-packs/_bundled/g7-core-{locale}/`)가 존재해야 프로비저닝으로 채울 수 있습니다. 대응 소스가 없는 로케일을 선언하면 설치할 팩 자체가 없어 그 로케일이 조용히 `ko` 로 폴백합니다(복구 경로 없음). 이 정합은 정적 검사로 확인됩니다 — 소스를 추가하거나 미지원 로케일을 선언에서 제거하세요.
|
||
|
||
## 시더 통합 (HookManager 필터)
|
||
|
||
기존 시더에 `applyFilters` 1줄만 추가하면 자동으로 다국어 키가 병합됩니다.
|
||
|
||
```php
|
||
// database/seeders/RolePermissionSeeder.php
|
||
$config = config('core.permissions');
|
||
$config = HookManager::applyFilters('core.permissions.config', $config);
|
||
foreach ($config['categories'] as $cat) { ... }
|
||
```
|
||
|
||
| 필터명 | 적용 위치 |
|
||
|---|---|
|
||
| `core.permissions.config` | RolePermissionSeeder |
|
||
| `core.roles.config` | RolePermissionSeeder |
|
||
| `core.menus.config` | CoreAdminMenuSeeder |
|
||
| `seed.notifications.translations` | NotificationDefinitionSeeder |
|
||
| `seed.identity_messages.translations` | IdentityMessageDefinitionSeeder (본인인증 메일 메시지 정의 — provider × scope × scope_value 복합 키) |
|
||
| `seed.{vendor-module}.{entity}.translations` | 모듈 시더 (예: `seed.sirsoft-ecommerce.shipping_types.translations`, `seed.sirsoft-ecommerce.notifications.translations`, `seed.sirsoft-board.board_types.translations`) |
|
||
| `seed.{vendor-module}.identity_messages.translations` | 모듈/플러그인 IDV 메시지 정의 — `ModuleManager::syncModuleIdentityMessages` / `PluginManager::syncPluginIdentityMessages` 가 발화 |
|
||
| `module.{id}.admin_menus.translations` | ModuleManager::createModuleMenus() — 모듈 admin_menus 동기화 시 |
|
||
| `module.{id}.roles.translations` | ModuleManager::createModuleRoles() — 모듈 roles 동기화 시 |
|
||
| `module.{id}.permissions.translations` | ModuleManager::createModulePermissions() — 모듈 권한 트리 동기화 시 |
|
||
| `plugin.{id}.roles.translations` | PluginManager::createPluginRoles() — 플러그인 roles 동기화 시 |
|
||
| `plugin.{id}.permissions.translations` | PluginManager::createPluginPermissions() — 플러그인 권한 트리 동기화 시 |
|
||
| `template.language.merge` | TemplateService::getLanguageDataWithModules() |
|
||
|
||
**확장 시드 필터 자동 결선** (`LanguagePackServiceProvider::registerExtensionSeedFilters`): 활성 모듈/플러그인 언어팩의 `seed/*.json` 마다 위 패턴의 필터에 listener 가 자동 등록됩니다. 매칭 키는 entry 의 컬럼을 우선순위(`code` > `slug` > `key` > `identifier` > `id`) 로 자동 감지합니다.
|
||
|
||
엔티티별 결선 정책:
|
||
- `notifications.json` → `seed.{target}.notifications.translations` 필터 (Definition × Template 3-tier 구조)
|
||
- `identity_messages.json` → `seed.{target}.identity_messages.translations` 필터 (Definition × Template × Channel — provider/scope_type/scope_value 복합 키 매칭)
|
||
- `menus.json` → `module.{target}.admin_menus.translations` 필터 (모듈 전용)
|
||
- `roles.json` → `{scope}.{target}.roles.translations` 필터 (module/plugin)
|
||
- `permissions.json` → `{scope}.{target}.permissions.translations` 필터 (3-레벨 트리: module/categories/permissions)
|
||
- 기타 → `seed.{target}.{entity}.translations` 필터 (단순 entity 시드)
|
||
|
||
### 복원 경로의 언어팩 병합
|
||
|
||
[기본값 복원](알림 템플릿·본인인증 메시지)은 위 시딩 필터가 아니라 자체 기본값 필터
|
||
(`core.notification.filter_default_definitions` / `core.identity.filter_default_message_definitions`)를
|
||
탄다. 복원 기본값에는 **활성 언어팩 seed 로케일이 반드시 병합**되어야 한다 — 병합하지 않으면
|
||
복원이 팩이 주입해 둔 로케일(ja 등)을 config 의 ko/en 만으로 대체해 영구 소실시키며,
|
||
오류도 로그도 남지 않는다. 코어 서비스(NotificationTemplateService·IdentityMessageTemplateService)가
|
||
필터 적용 후 시딩과 같은 주입기(SSoT)로 병합하며, 정적 검사(테스트)가 이 계약을 고정한다.
|
||
|
||
시딩 필터의 페이로드는 **발화 주체의 원형 키를 보존**해야 한다 — 코어 알림 시더는 config
|
||
원형(연관 배열, 키가 곧 type), 본인인증은 복합 키(`mail.purpose.signup` 등)가 매칭 키다.
|
||
키를 버리고 리스트로 만들면 주입기가 전 항목을 스킵해 팩 로케일이 오류 없이 주입되지 않는다.
|
||
알림 주입기는 두 형태(문자열 키 우선, 없으면 type 필드)를 수용한다.
|
||
|
||
### 사용자 수정 보존 판정과 시딩 컨텍스트
|
||
|
||
언어팩 병합·제거의 사용자 수정 보존 판정은 운영자 수정의 실제 기록 형식인 **dot-path 항목**
|
||
(`user_overrides` 의 `"{컬럼}.{로케일}"`)을 기준으로 한다. 컬럼 전체 항목(`"subject"`)은 시더
|
||
재실행의 컬럼 보호 선언이며 팩 병합을 막지 않는다 — 막으면 팩 업데이트가 자기 로케일 번역을
|
||
갱신하지 못한다.
|
||
|
||
이를 위해 두 가지가 함께 지켜져야 한다:
|
||
|
||
- 다국어 JSON 컬럼을 추적하는 모델은 `translatableTrackableFields` 를 선언한다 — 미선언 시
|
||
운영자 수정이 컬럼 전체 항목으로 기록되어 로케일 단위 보존이 불가능해진다.
|
||
- 언어팩 주입·제거처럼 시스템이 수행하는 저장은 시딩 컨텍스트(`user_overrides.seeding` 바인딩)
|
||
안에서 실행한다 — 바인딩 없이 저장하면 주입 자체가 "사용자 수정"으로 오인 기록되어 이후
|
||
시더 재실행의 컬럼 갱신이 영구히 얼어붙는다.
|
||
|
||
### 실패한 업데이트의 상태 복원
|
||
|
||
`language-pack:update` 가 설치 트랜잭션 이후(활성화 단계)에 실패하면 팩 상태를 이전 상태로
|
||
복원한다. 복원하지 않으면 active 였던 팩이 installed 로 방치되어 해당 로케일의 백엔드 번역이
|
||
안내 없이 기본 로케일로 폴백된다.
|
||
|
||
### 같은 프로세스 활성화와 필터 인스턴스
|
||
|
||
시더 번역 필터 클로저는 boot 시점의 injector→registry **인스턴스**를 캡처한다. 활성 팩 목록
|
||
캐시를 갱신할 때는 싱글톤을 유지한 채 `LanguagePackRegistry::invalidate()` 로 내부 캐시만
|
||
비운다 — `forgetInstance` 로 바인딩을 교체하면 캡처된 구 인스턴스의 stale 캐시가 남아, 같은
|
||
프로세스에서 활성화된 신규 팩이 시더 필터에 보이지 않는다(오류 없이 해당 로케일만 미주입).
|
||
|
||
### IDV 도메인 lang pack 커버리지
|
||
|
||
`config/core.php` 의 본인인증 SSoT 4개 블록 중 lang pack seed 대상은 1개:
|
||
|
||
| SSoT 블록 | lang pack seed | 사유 |
|
||
|---|---|---|
|
||
| `notification_definitions` | ✓ `seed/notifications.json` | 정의/템플릿이 다국어 데이터 직접 보유 |
|
||
| `identity_messages` | ✓ `seed/identity_messages.json` | 메일 본문/제목이 다국어 데이터 직접 보유 |
|
||
| `identity_policies` | ❌ | `IdentityPolicy` 모델에 다국어 필드 부재 — 관리자 UI 라벨은 `lang/{locale}/identity.php` 또는 `frontend/partial/admin.json` 의 i18n 키로 처리 |
|
||
| `identity_purposes` | ❌ | `label_key`/`description_key` 참조 패턴 — 실제 ja 라벨은 `frontend/partial/admin.json::identity.purposes.*` 키로 처리 |
|
||
|
||
**`identity_messages.json` 스키마 예시**:
|
||
|
||
```json
|
||
{
|
||
"mail.purpose.signup": {
|
||
"definition": {
|
||
"name": "会員登録認証",
|
||
"description": "会員登録時のメール認証"
|
||
},
|
||
"templates": {
|
||
"mail": {
|
||
"subject": "[アプリ] 会員登録認証コード",
|
||
"body": "<p>認証コード: {code}</p>"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
복합 키는 `{channel}.{scope_type}.{scope_value}` 또는 `{channel}.provider_default` 형태 — config/core.php::identity_messages 의 array key 와 동일.
|
||
|
||
**확장 권한 트리 시드 키 포맷**: 권한은 ModuleManager 가 DB row identifier 를 `{module-id}.{cat-id}.{perm-id}` 로 prefix 하므로 시드도 동일 포맷을 사용합니다. 권한 노드 식별자는 `action`(이커머스 등) 또는 `identifier`(명시적) 둘 중 존재하는 키를 사용합니다.
|
||
|
||
## user_overrides 보존 정책
|
||
|
||
`HasUserOverrides` 사용 모델(Permission/Role/Menu/NotificationDefinition/NotificationTemplate/Module/Plugin/Template)에서 사용자가 직접 수정한 컬럼/locale 키는 언어팩이 덮어쓰지 않습니다.
|
||
|
||
| 케이스 | 동작 |
|
||
|---|---|
|
||
| A: 기존 row 에 해당 locale 키 부재 | 추가 (overrides 무관) |
|
||
| B: locale 키 존재 + user_overrides 등록 | **건너뜀** (사용자 보존) |
|
||
| C: locale 키 존재 + user_overrides 미등록 | 시드 값으로 덮어쓰기 |
|
||
|
||
비활성화/제거 시에도 user_overrides 등록 locale 은 JSON 에서 제거되지 않습니다. 모든 보존 결정은 `language_pack` 채널에 감사 로그로 기록됩니다.
|
||
|
||
## 백엔드 번역 namespace fallback 메커니즘
|
||
|
||
Laravel `Translator` 의 FileLoader 는 `addNamespace($namespace, $hint)` 가 단일 hint 만 보유하며 덮어쓰는 구조이므로, 모듈/플러그인 자체의 `src/lang` namespace 등록 후 같은 namespace 로 언어팩이 추가되면 모듈의 ko/en 등록이 통째로 사라지는 회귀가 발생합니다.
|
||
|
||
`LanguagePackTranslator` 가 이를 회피하기 위한 namespace fallback 경로를 별도 보유합니다.
|
||
|
||
- `addNamespaceFallbackPath($namespace, $locale, $path)` — namespace ⇒ locale ⇒ 경로 배열로 누적 등록
|
||
- `load($namespace, $group, $locale)` — 표준 hint 로 1차 로드 후 namespace fallback 경로의 PHP 배열 파일을 누락 키만 보완 병합 (기존 번역 우선)
|
||
|
||
이 메커니즘으로 모듈은 `src/lang/{ko,en}/*.php` 의 표준 namespace 등록을 그대로 유지하고, 활성 ja 언어팩은 fallback 경로(`lang-packs/g7-module-{id}-ja/backend/ja/`) 에서만 ja 키를 보완합니다.
|
||
|
||
## 프론트엔드 다국어 병합의 wrap 정책
|
||
|
||
`MergeFrontendLanguage` 가 활성 언어팩의 `frontend/*.json` 을 병합할 때 scope 별로 root key 처리가 다릅니다.
|
||
|
||
- core / template scope — root 에 평탄 병합 (TemplateService 의 ko 데이터 구조와 일치)
|
||
- module / plugin scope — `[$pack->target_identifier => $frontend]` 로 wrap 후 병합
|
||
|
||
wrap 이 필요한 이유: TemplateService 가 모듈/플러그인 자체 ko 데이터를 `[$identifier => $data]` 로 wrap 하므로, 언어팩 데이터도 동일 구조여야 `$t:sirsoft-ecommerce.admin.*` 같은 표현식이 ja 활성 시에도 정확한 경로로 해석됩니다.
|
||
|
||
## 권한
|
||
|
||
| 권한 | 설명 |
|
||
|---|---|
|
||
| `core.language_packs.read` | 언어팩 목록 및 상세 조회 |
|
||
| `core.language_packs.install` | ZIP/GitHub/URL 설치 |
|
||
| `core.language_packs.manage` | 활성화/비활성화/제거 |
|
||
|
||
## REST API
|
||
|
||
| 엔드포인트 | 메서드 | 권한 |
|
||
|---|---|---|
|
||
| `/api/admin/language-packs` | GET | `read` |
|
||
| `/api/admin/language-packs/{id}` | GET | `read` |
|
||
| `/api/admin/language-packs/install-from-file` | POST | `install` |
|
||
| `/api/admin/language-packs/install-from-github` | POST | `install` |
|
||
| `/api/admin/language-packs/install-from-url` | POST | `install` |
|
||
| `/api/admin/language-packs/{id}/activate` | POST | `manage` |
|
||
| `/api/admin/language-packs/{id}/deactivate` | POST | `manage` |
|
||
| `/api/admin/language-packs/{id}` | DELETE | `manage` |
|
||
|
||
## 보안
|
||
|
||
1. **ZIP slip 방지** — `ZipInstallHelper` 의 기존 검증 재사용
|
||
2. **PHP 격리** — `backend/` 디렉토리 외 PHP 파일 거부
|
||
3. **위험 함수 차단** — `eval/include/require/exec/system/popen` 등 정적 분석으로 거부
|
||
4. **파일 크기 제한** — 10MB
|
||
5. **체크섬 검증** — URL 설치 시 SHA-256 옵션
|
||
|
||
## 외부 패키지형 vs 메타데이터 전용 번들
|
||
|
||
번들 언어팩은 두 가지 형태를 가질 수 있습니다.
|
||
|
||
| 형태 | `contents` | 실제 번역 자산 위치 | 사용 사례 |
|
||
|---|---|---|---|
|
||
| **메타데이터 전용** | 빈 배열 | 코어/확장 트리(`lang/{locale}/`, `templates/_bundled/*/lang/{locale}.json`) | 코어 fallback locale (ko/en) — 트리 자체에 포함된 자산을 시스템에 등록만 하기 위함 |
|
||
| **외부 패키지형** | 실제 파일 경로 | 패키지 디렉토리 내(`backend/{locale}/*.php`, `frontend/*.json`, `seed/*.json`) | 추가 로케일 (ja, zh-CN 등) — 코어 트리를 건드리지 않고 새 locale 추가 |
|
||
|
||
기본 ko/en 은 메타데이터 전용 — 실제 PHP/JSON 파일은 코어 트리에 그대로 두고 manifest 1개로 시스템에 등록만 합니다. 새 locale (예: 일본어) 은 외부 패키지형으로 도입하여 **코어 코드 변경 0** 을 유지합니다.
|
||
|
||
## 공식 일본어 번들 언어팩 (g7-*-ja)
|
||
|
||
G7 는 일본어 번들 언어팩 12종을 공식 제공합니다. ko 원본을 LLM 으로 자동 번역하여 빌드된 산출물이며, 사용자 환경에서는 일반 번들 확장과 동일하게 `language-pack:install --source=bundled` 로 설치합니다.
|
||
|
||
### 패키지 구성
|
||
|
||
| scope | 식별자 | target_identifier |
|
||
|---|---|---|
|
||
| core | `g7-core-ja` | (null) |
|
||
| template | `g7-template-sirsoft-admin_basic-ja` | sirsoft-admin_basic |
|
||
| template | `g7-template-sirsoft-basic-ja` | sirsoft-basic |
|
||
| template | `g7-template-gnuboard7-hello_admin_template-ja` | gnuboard7-hello_admin_template |
|
||
| template | `g7-template-gnuboard7-hello_user_template-ja` | gnuboard7-hello_user_template |
|
||
| module | `g7-module-sirsoft-ecommerce-ja` | sirsoft-ecommerce |
|
||
| module | `g7-module-sirsoft-board-ja` | sirsoft-board |
|
||
| module | `g7-module-sirsoft-page-ja` | sirsoft-page |
|
||
| module | `g7-module-gnuboard7-hello_module-ja` | gnuboard7-hello_module |
|
||
| plugin | `g7-plugin-sirsoft-ckeditor5-ja` | sirsoft-ckeditor5 |
|
||
| plugin | `g7-plugin-sirsoft-marketing-ja` | sirsoft-marketing |
|
||
| plugin | `g7-plugin-sirsoft-tosspayments-ja` | sirsoft-tosspayments |
|
||
|
||
### 입력 → 산출 매핑
|
||
|
||
| 패키지 | ko 원문 소스 | 산출 위치 |
|
||
|---|---|---|
|
||
| `g7-core-ja` backend | `lang/ko/*.php` (33개) | `lang-packs/_bundled/g7-core-ja/backend/ja/*.php` |
|
||
| `g7-core-ja` seed | `config/core.php` (permissions/roles/menus) + `NotificationDefinitionSeeder` | `seed/{permissions,roles,menus,notifications}.json` |
|
||
| `g7-template-{tpl}-ja` | `templates/_bundled/{tpl}/lang/ko.json` + `lang/partial/ko/*.json` | `frontend/ja.json` + `frontend/partial/*.json` |
|
||
| `g7-module-{mod}-ja` | `modules/_bundled/{mod}/{src,resources}/lang/ko/*.php` + `resources/lang/ko.json` + `resources/lang/partial/ko/**/*.json` (재귀) | `backend/ja/*.php` + `frontend/ja.json` + `frontend/partial/**/*.json` |
|
||
| `g7-module-sirsoft-ecommerce-ja` seed | `ShippingTypeSeeder`, `ClaimReasonSeeder`, `EcommerceNotificationDefinitionSeeder` ko 추출 | `seed/{shipping_types,claim_reasons,notifications}.json` |
|
||
| `g7-module-sirsoft-board-ja` seed | `BoardTypeSeeder::DEFAULT_BOARD_TYPES` 상수 추출 | `seed/board_types.json` |
|
||
| 모듈 ja 팩 (공통) | `module.php` 의 `getAdminMenus` / `getRoles` / `getPermissions` 추출 | `seed/{menus,roles,permissions}.json` |
|
||
| 플러그인 ja 팩 (공통) | `plugin.php` 의 `getRoles` / `getPermissions` 추출 | `seed/{roles,permissions}.json` |
|
||
| 모든 확장 ja 팩 | `module.json` / `plugin.json` / `template.json` 의 `name`/`description` ko | `seed/manifest.json` |
|
||
| `g7-plugin-{pl}-ja` | `plugins/_bundled/{pl}/lang/ko/*.php` | `backend/ja/*.php` |
|
||
|
||
### 빌드 source kind
|
||
|
||
| kind | 동작 |
|
||
|---|---|
|
||
| `php-dir` | 디렉토리의 모든 *.php 를 ko → ja 로 번역 |
|
||
| `json-file` | 단일 JSON 파일 ($partial 디렉티브 경로 정규화 포함) |
|
||
| `json-dir` | 디렉토리의 모든 *.json **재귀** (admin/* 같은 서브디렉토리 포함) |
|
||
| `core-seed` | `config/core.php` 에서 permissions/roles/menus ko 추출 |
|
||
| `notif-seed` | NotificationDefinitionSeeder 의 getDefaultDefinitions 추출 |
|
||
| `ext-seed` | 확장 시더의 메서드/상수 추출 (entity 별 매칭 키 자동 감지) |
|
||
| `ext-spec-seed` | module.php / plugin.php 의 getAdminMenus / getRoles / getPermissions 결과 추출. 권한은 `{module-id}.{cat-id}.{perm-id}` prefixed identifier 로 출력 |
|
||
| `manifest-seed` | manifest 파일의 name/description ko 추출 → seed/manifest.json |
|
||
|
||
### 번역 규칙
|
||
|
||
- placeholder 보존: `:attribute`, `{count}`, `{{var}}`, `%s`, `:other`
|
||
- HTML 태그 보존: `<a>`, `<br>`, `<strong>`, `<span>`
|
||
- 키(key) 변형 금지 — value 만 번역
|
||
- 통화/숫자 단위 변환 금지 (런타임 처리)
|
||
- 공식체(です·ます) 강제, 친근체 금지
|
||
- 한글 미포함 value(영문/숫자/기호만) 는 번역 스킵 → 원문 유지
|
||
- 고정 용어집으로 일관성 강제 (예: `관리자 → 管理者`, `장바구니 → カート`)
|
||
|
||
### 용어집 등록 의무
|
||
|
||
번역 결과의 표기는 고정 용어집이 정한다. 용어집에 없는 도메인 용어는 어간만 번역되어
|
||
**원본 로케일 문자가 값에 그대로 남는다** — 예외도 오류도 나지 않고, 키 대칭 검사도 값은 보지 않으므로
|
||
그 로케일로 화면을 열기 전까지 드러나지 않는다.
|
||
|
||
- 새 도메인 용어를 도입하면 번역 빌드 **전에** 용어집에 등록한다
|
||
- 이미 생성된 값을 손으로 고쳤다면 용어집도 함께 고친다 — 용어집을 두면 다음 키에서 같은 오역이 되풀이된다
|
||
- 같은 용어는 팩 전체에서 한 표기로 통일한다 (용어집이 단일 출처)
|
||
|
||
정적 검사가 번역 값의 원본 로케일 문자 잔존을 검출한다. 매니페스트와 CHANGELOG,
|
||
그리고 한국어 주석은 대상이 아니다.
|
||
|
||
### 설치 및 활성화
|
||
|
||
```bash
|
||
# 12개 패키지를 번들 소스로 설치 (자동 활성)
|
||
php artisan language-pack:install g7-core-ja --source=bundled
|
||
php artisan language-pack:install g7-template-sirsoft-admin_basic-ja --source=bundled
|
||
# ... (나머지 10개 동일)
|
||
|
||
# 확인
|
||
php artisan language-pack:list --scope=core
|
||
php artisan language-pack:list --scope=module
|
||
```
|
||
|
||
### 기존 ko/en 변경 시 번들 ja 동기화 의무
|
||
|
||
코어/모듈/플러그인/템플릿의 ko 또는 en 다국어 키를 추가/수정/제거할 때마다 대응하는 번들 ja 패키지(`lang-packs/_bundled/g7-*-ja/`) 의 키 셋을 동기화해야 한다. 동기화하지 않으면 일본어 화면에서 미번역 fallback(ko/en) 이 노출되어 UX 가 손상된다.
|
||
|
||
번들 ja 동기화/빌드 자동화 도구는 메인테이너 영역이며, 외부 기여자는 ko 원본 변경 후 PR 시 메인테이너가 동기 빌드를 수행한다. 자기 환경에서 검증이 필요하다면 ko 키를 직접 ja 패키지의 대응 위치(`lang-packs/_bundled/g7-*-ja/backend/ja/`, `frontend/`, `seed/`) 에 수동 추가/수정/삭제할 수 있다 — 키만 일치하면 시스템이 정상 fallback 한다.
|
||
|
||
#### 잉여 키 (ko 에서 제거 후 번들 잔류) 처리
|
||
|
||
ko 에서 키 제거 시 번들 ja 의 대응 키도 수동 제거 권장 (자동 제거 도구 없음 — 의도치 않은 제거 방지).
|
||
|
||
#### 번들 언어팩 버전 + CHANGELOG 작성 규정
|
||
|
||
번들 언어팩 자산을 수정할 때마다 모듈/플러그인/템플릿 확장과 동일한 수준의 버전 + CHANGELOG 관리를 적용한다.
|
||
|
||
- **버전 bump 시점**: 패키지가 외부에 한 번이라도 출시(릴리즈/배포) 된 이후의 수정에 한해 버전을 올린다. 아직 출시되지 않은 번들 패키지의 사전 보강(키 추가/번역 정정 등) 은 `1.0.0` 그대로 유지한다. 이미 출시된 버전의 콘텐츠를 수정하면 patch bump 가 필수다 (출시 확정 버전의 콘텐츠를 버전 유지한 채 덮어쓰지 않는다).
|
||
- **버전 bump 단위**: 출시 후 키 추가/누락 보강은 패치 (1.0.0 → 1.0.1), 의미적으로 큰 변경이나 제거는 마이너 (1.0.0 → 1.1.0).
|
||
- **CHANGELOG.md**: 패키지 루트에 `CHANGELOG.md` 작성. Keep a Changelog 표준 (`## [버전] - YYYY-MM-DD` + `### Added/Changed/Fixed/Removed`).
|
||
- **톤**: 사용자 관점, 1~2줄 불릿. 내부 파일 경로/내부 함수명/이슈 번호 미기재.
|
||
- **신규 기능의 개발 중 결함**: 같은 릴리즈에 처음 도입된 키의 누락 보강 등은 Fixed 가 아닌 Added 로 기록.
|
||
- **번역 자체 수정** (예: 어색한 표현 교정): `### Changed` 로 기록.
|
||
- **자동화**: 빌드 스크립트는 기존 manifest 의 version 을 읽어 보존하므로, 수동 bump 한 1.0.1 등이 다음 빌드에서 1.0.0 으로 덮어쓰이지 않는다.
|
||
|
||
### 외부 언어팩 패키지 개발 · 배포
|
||
|
||
번들 ja 와 동일한 디렉토리 구조의 언어팩 패키지를 자체 작성하여 별도 배포할 수 있다. 코어 트리는 변경하지 않으므로 코어 업데이트와 충돌하지 않는다.
|
||
|
||
```text
|
||
my-lang-pack-zh/
|
||
├── language-pack.json # 매니페스트 (식별자, scope, target_identifier, locale, version, manifest 키 셋)
|
||
├── backend/
|
||
│ └── zh-CN/ # 백엔드 PHP 키 (스코프가 코어/모듈/플러그인일 때)
|
||
│ └── *.php
|
||
├── frontend/ # 프론트엔드 JSON 키 (스코프가 모듈/플러그인/템플릿일 때)
|
||
│ ├── zh-CN.json
|
||
│ └── partial/
|
||
│ └── *.json
|
||
└── seed/ # 초기 데이터 번역 (스코프별 entity 키 매칭 — permissions/menus/notifications 등)
|
||
└── *.json
|
||
```
|
||
|
||
매니페스트(`language-pack.json`) 필드는 번들 ja 패키지를 참고. `scope` 는 `core`/`module`/`plugin`/`template` 중 하나, `target_identifier` 는 대상 확장 식별자(코어 스코프는 null), `locale` 은 [BCP 47](https://tools.ietf.org/html/bcp47) 코드.
|
||
|
||
### 설치
|
||
|
||
```bash
|
||
# 로컬 디렉토리에서 설치
|
||
php artisan language-pack:install /path/to/my-lang-pack-zh --source=local
|
||
|
||
# 또는 zip 으로 배포 후 URL 에서 설치
|
||
php artisan language-pack:install https://example.com/my-lang-pack-zh.zip --source=url
|
||
```
|
||
|
||
설치 후 `language-pack:list` 로 확인. 다른 로케일 패키지와 동일하게 활성/비활성/제거가 가능하며, 의존성 검증(`core_locale_missing` 등) 도 동일하게 적용된다.
|
||
|
||
## 의존성 검증 — 설치 차단 사유 (UI 인라인 안내)
|
||
|
||
미설치 번들 행과 설치 모달은 다음 4가지 사유로 설치 차단을 표시한다 (`LanguagePackService::resolveInstallBlockedReason()` 가 단일 SSoT). 모듈/플러그인 시스템과 동일 강도.
|
||
|
||
| 사유 키 | 발생 조건 |
|
||
| ------- | --------- |
|
||
| `core_locale_missing` | scope ≠ core + 코어 동일 locale 팩 미활성 |
|
||
| `target_not_installed` | 대상 모듈/플러그인/템플릿이 DB 에 없음 |
|
||
| `target_inactive` | 대상은 설치되었지만 status ≠ active |
|
||
| `target_version_too_old` | manifest `requires.target_version` 미충족 |
|
||
|
||
`LanguagePackResource::resourceMeta()` 가 차단 사유 존재 시 `abilities.can_install` 을 false 로 강제 — UI 행/모달 버튼이 권한과 무관하게 disabled.
|
||
|
||
## is_protected 정책 (모듈/플러그인/템플릿과 일관)
|
||
|
||
번들 언어팩(`lang-packs/_bundled/*`) 은 manifest 의 `is_protected` 선언을 따른다. 기본값은 `false` — 모듈/플러그인/템플릿이 번들이라도 자유 제거 가능한 것과 동일.
|
||
|
||
`LanguagePackBundledRegistrar` 가 동기화하는 `bundled_with_extension` 레코드(모듈/플러그인/템플릿 자체에 포함된 lang 디렉토리에서 자동 등록되는 가상 행) 만 `is_protected: true` 로 고정 — 부모 확장 lifecycle 에 종속되어 독립 제거가 불가하기 때문.
|
||
|
||
## Manifest 미리보기 (3종 확장과 동등)
|
||
|
||
ZIP 업로드 전 manifest 검증 결과를 사전 확인할 수 있다. 모듈/플러그인/템플릿 시스템에도 동일 패턴으로 추가됨.
|
||
|
||
- 라우트: `POST /api/admin/{language-packs|modules|plugins|templates}/manifest-preview`
|
||
- 응답: `{ manifest, validation: { errors[], is_valid, already_installed, existing_version } }`
|
||
- 검증 실패 시에도 HTTP 200 — `validation.is_valid: false` + `errors[]` 노출
|
||
- UI: 각 도메인의 수동 설치 모달(`_modal_manual_install.json`) 에 "manifest 미리보기" 버튼 → 별도 드로어 partial(`_drawer_manifest_preview.json`) 로 ZIP 업로드 + 결과 표시
|
||
- 드로어 작성 패턴: [modal-usage.md ZIP 파일 업로드 모달/드로어 패턴](../frontend/modal-usage.md#zip-파일-업로드-모달드로어-패턴-multipart-백엔드)
|
||
|
||
## 관련 파일
|
||
|
||
- `app/Models/LanguagePack.php`
|
||
- `app/Services/LanguagePackService.php`
|
||
- `app/Services/LanguagePack/LanguagePackRegistry.php`
|
||
- `app/Services/LanguagePack/LanguagePackManifestValidator.php`
|
||
- `app/Services/LanguagePack/LanguagePackSeedInjector.php`
|
||
- `app/Services/LanguagePack/LanguagePackTranslator.php`
|
||
- `app/Listeners/LanguagePack/SyncDatabaseTranslations.php`
|
||
- `app/Listeners/LanguagePack/MergeFrontendLanguage.php`
|
||
- `app/Providers/LanguagePackServiceProvider.php`
|
||
- `app/Http/Controllers/Api/Admin/LanguagePackController.php`
|
||
- `database/migrations/2026_04_27_000001_create_language_packs_table.php`
|
||
- `templates/_bundled/sirsoft-admin_basic/layouts/admin_language_pack_list.json`
|