@gaonjs/cli 0.64.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/check.js +34 -18
- package/dist/commands/g.d.ts +1 -0
- package/dist/commands/g.js +11 -2
- package/dist/commands/gen.js +1 -1
- package/dist/dev.d.ts +13 -7
- package/dist/dev.js +30 -22
- package/dist/doctor/fixers/i18n-layout.d.ts +2 -2
- package/dist/doctor/fixers/i18n-layout.js +119 -25
- package/dist/doctor/fixers/index.js +7 -2
- package/dist/doctor/fixers/types.d.ts +17 -2
- package/dist/doctor/i18n-app-scope.d.ts +3 -0
- package/dist/doctor/i18n-app-scope.js +94 -65
- package/dist/doctor/i18n-layout.d.ts +1 -1
- package/dist/doctor/i18n-layout.js +157 -32
- package/dist/doctor/i18n-server-scope.d.ts +2 -0
- package/dist/doctor/i18n-server-scope.js +124 -0
- package/dist/doctor/locale-parity.js +29 -68
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.js +36 -10
- package/dist/i18n-config.d.ts +9 -9
- package/dist/i18n-config.js +8 -15
- package/dist/messages-gen.d.ts +10 -10
- package/dist/messages-gen.js +42 -54
- package/dist/scaffold/app.d.ts +10 -1
- package/dist/scaffold/app.js +12 -2
- package/dist/templates/project/AGENTS.md.tpl +7 -6
- package/dist/templates/project/agents/i18n.md.tpl +165 -70
- package/dist/templates/project/agents/web.md.tpl +4 -3
- package/dist/templates/project/{locales → apps/web/locales}/en/frontend.json.tpl +1 -0
- package/dist/templates/project/{locales → apps/web/locales}/ko/frontend.json.tpl +1 -0
- package/dist/templates/project/gaon.config.ts.tpl +6 -5
- package/package.json +7 -7
- package/dist/templates/project/apps/web/locales/en.json.tpl +0 -3
- package/dist/templates/project/apps/web/locales/ko.json.tpl +0 -3
- /package/dist/templates/project/{locales → domain/locales}/en/backend.json.tpl +0 -0
- /package/dist/templates/project/{locales → domain/locales}/ko/backend.json.tpl +0 -0
|
@@ -80,7 +80,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
80
80
|
혼용 금지 · doctor response-mixing).
|
|
81
81
|
8. **`.gaon/` 자동 생성 파일 편집 금지** — `routes.d.ts`·`routes.manifest.ts`
|
|
82
82
|
(routes 축 = 타입 브리지 + api() 런타임 매니페스트 2파일 · 결정 127)·
|
|
83
|
-
`tables.d.ts`·`messages.d.ts`(3축 ·
|
|
83
|
+
`tables.d.ts`·`messages.d.ts`(3축 · 카탈로그 있을 때 · 결정 158)·`env.d.ts`
|
|
84
84
|
(`.env` 스캔 · 프론트 앱 · 결정 198) 는 `gaon check`/`gaon dev`/`gaon gen` 이
|
|
85
85
|
재생성한다.
|
|
86
86
|
|
|
@@ -113,7 +113,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
113
113
|
컬럼명 · 스키마 파일 ↔ 테이블 ↔ `tables.d.ts` 키 변환 규칙)은
|
|
114
114
|
`agents/data.md` "DB 네이밍" 표가 정본이다 — 먼저 읽는다.
|
|
115
115
|
|
|
116
|
-
### 2.2 `gaon doctor` 검사
|
|
116
|
+
### 2.2 `gaon doctor` 검사 36종
|
|
117
117
|
|
|
118
118
|
1. `response-mixing` — 한 액션 안 render/JSON/redirect 혼용 (E-3)
|
|
119
119
|
2. `n-plus-one` — include 미사용 · loop 안 관계 호출 (E-4)
|
|
@@ -141,15 +141,16 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
141
141
|
24. `seal-security` — `@gaonjs/seal` 을 켠 앱에서 (a) `gaon.config.ts` 가 진짜 방어층(rate limit·보안 헤더·CORS)을 **명시적으로 껐을** 때 = 봉인을 켜고 방어를 끄는 역전 **경고**, (b) `main.ts` 가 seal 클라이언트를 배선(`@gaonjs/seal/client` 정적 import + `createGaonApp` sealClient)하지 않았을 때 = 봉인 문서를 브라우저가 못 열어 blank 가 되는 **에러**(`gaon doctor --fix --yes` 의 `seal-client-wiring` fixer 가 자동 배선). seal 은 서버 검증을 대체하지 않는다 (결정 121·124 · `agents/seal.md`)
|
|
142
142
|
25. `schema-relations` — 커넥션을 가로지르는 belongsTo·역방향 관계(SQL 조인이 커넥션을 못 넘음)와 존재하지 않는 관계 대상 = **에러**(§4.5). **파티션 키 컬럼이 실제 컬럼인지도 검사**(결정 277 · `checkPartitions` — 오타·유령 컬럼). data 패키지 검사(`checkCrossConnectionRelations`·`checkRelationTargets`·`checkPartitions`)를 CLI 러너가 배선 — 배포 후 raw postgres 에러 대신 doctor 가 잡는다 (결정 134·277 · `agents/data.md`)
|
|
143
143
|
26. `no-import-meta-env` — `.vue`(SFC) `<script>` 에서 `import.meta.env` 직접 사용 = **에러**. SFC 는 nodenext 아래 CommonJS 출력으로 분류돼 vue-tsc 가 TS1470 로 거부한다(`gaon check` red). 클라 공개 환경변수는 `import { env } from 'gaonjs/vue'` 로 읽으라(VITE_* 접두 제거·타입드 · `.gaon/env.d.ts` 는 `.env` 스캔 생성) — 템플릿 프로즈·주석의 언급은 오탐 제외 (결정 198 · `agents/frontend.md` §9)
|
|
144
|
-
27. `locale-parity` —
|
|
144
|
+
27. `locale-parity` — **같은 스코프 안에서 로케일 간** 키 부분 누락 = **경고**. 어떤 키가 특정 로케일에만 빠지면 `messages.d.ts`(기준 로케일 기준)는 컴파일을 통과하고, 런타임에 그 로케일 사용자는 fallback(서버) 또는 키 문자열(클라 · 결정 455)을 본다. 비교 단위 3종 — ① `domain/locales` backend ② 앱별 frontend ③ 앱별 backend. **스코프·소유자를 가로지르는 비교는 하지 않는다**(앱 전용 키가 전량 오탐). 복수형 접미사는 base 로 정규화해 비교한다. 로케일이 0·1개면 무소음 (`--json` 은 `detail.missing`·`detail.scope`·`detail.owner` · 결정 216·456·459 · `agents/i18n.md` §9.1)
|
|
145
145
|
28. `render-return` — 액션이 `this.render`/`this.redirect`/`this.json` 을 호출만 하고 `return` 하지 않음 = 응답이 버려져 조용히 204(백지) — `return this.render(...)` 로 고치라 (결정 340 · 경고)
|
|
146
146
|
29. `channel-collision` — 두 앱이 **같은 이름의 채널**을 각각 정의 = **에러**. 채널 이름은 전역이다(브로드캐스트 subject `gaon.chan.<이름>`·프레즌스 키에 앱 프리픽스 없음) — 한 앱의 broadcast 가 다른 앱 연결로 팬아웃되고 접속자 목록이 병합되며, 두 정의의 `authorize` 가 갈리면 공개 쪽 규칙으로 메시지가 샌다. 앱마다 이름을 분리하거나(클라이언트 `useChannel` 인자도 함께), **여러 앱이 한 방을 공유**하는 것이 의도면 정의를 `domain/channels/<이름>.ts` 하나에 두고 각 앱 채널 파일에서 재수출하라(재수출은 통과 · 정의 하나 = 인가 규칙 하나 · **등록 파일은 `apps/<앱>/channels/` 에 그대로**). `shared/channels/` 는 정본이 아니다 — authorize 가 DB 를 보면 domain 값 import 가 필요해 `shared-purity`(#6)와 성립 불가이고, `shared/` 는 props 로만 받는 순수 UI 영역이다(§1-3). 옛 배치가 남아 있으면 같은 규칙이 **경고**로 회수한다 — 잡·리스너의 동명 등록 throw(결정 271)와 같은 계열의 정적 검사 (결정 456 · `agents/realtime.md` §2.10)
|
|
147
147
|
30. `channel-instance-authorize` — `instance: true` 채널(파라미터화 채널 · 결정 440)에 `authorize` 가 없음 = **경고**. 인스턴스 채널은 임의 문자열 키로 무한 실행 인스턴스(`/gaon/ws/<이름>/<인스턴스>`)가 열리므로, authorize 가 없으면 누구나 아무 인스턴스에나 입장한다. 매치·스레드처럼 참가자가 정해진 채널이면 `authorize(ctx)` 에서 `ctx.instance` 로 입장을 판정하라 — 공개 관전형(누구나 입장)이 의도면 무시해도 된다(재수출 정의는 대상 모듈을 따라가 판정 · `agents/realtime.md` §2.7)
|
|
148
148
|
31. `agents-docs-stale` — 이 문서(`AGENTS.md`)·`agents/*.md` 사본이 **설치된 gaonjs 템플릿(정본)과 다름** = **경고**(byte 비교). gaonjs 업그레이드 후 관례 문서가 옛 채로 남으면 AI 가 낡은 관례·없는 표면으로 코드를 짠다 — `gaon g agents-docs` 로 재동기하라(미리보기 `--check` · 멱등 · 사본에 직접 적은 내용은 프로젝트 소유 문서(CLAUDE.md)로 옮긴 뒤 — 이 문서들은 프레임웍 정본 사본이다) (결정 449)
|
|
149
149
|
32. `dotenv-node-env` — 공유 `.env`(·`.env.local`·`.env.example`)에 `NODE_ENV` 가 설정됨 = **경고**. **모드는 명령이 정한다** — `gaon dev` = development · `gaon serve` = production(결정 430). 이 파일들은 개발·운영이 함께 읽으므로 값을 박으면 모드가 양쪽으로 샌다: `production` 이면 `gaon dev` 가 쿠키 Secure·dev 플레이스홀더 secret 거부로 죽고, `development` 면 운영 `gaon serve` 에서 프로덕션 안전장치(플레이스홀더 secret 거부·쿠키 Secure·락 in-memory 폴백 차단)가 통째로 꺼진다(**부팅은 green, 보안만 꺼짐**). `.env` 에서 그 줄을 지우고, 모드별 값이 필요하면 `.env.development`/`.env.production` 오버레이에, 일회성이면 명령 앞에 붙인다(`NODE_ENV=production gaon serve`) — 모드별 오버레이 파일은 검사 대상이 아니다 (결정 430)
|
|
150
150
|
33. `page-fetch` — 세션 앱 `.vue` 가 앱 내부 경로를 raw `fetch('/...')` 로 호출 = **경고**. CSRF 토큰 자동 부착(결정 166·341·342)은 `api()`·`useForm`·`router` 를 탈 때만 작동해, raw fetch 의 상태 변경 요청(POST/PUT/PATCH/DELETE)은 403 으로 죽는다(rooms 샘플 실사용에서 강퇴/위임이 이 경로로 죽었다). JSON 액션(강퇴·위임·좋아요 등 커맨드 포함)은 `api()`, 폼은 `useForm`, 부득이한 커스텀 전송은 `readCsrfToken()` 탈출구 — 첫 인자가 `/` 로 시작하는 문자열/템플릿 리터럴만 검출(외부 URL·변수 인자 오탐 제외), JWT/API 앱은 REST + fetch 가 정본이라 제외 (결정 453 · `agents/frontend.md` §2)
|
|
151
|
-
34. `i18n-layout` —
|
|
152
|
-
35. `i18n-app-scope` — 앱 코드가 **그 앱 카탈로그에 없는** 클라 `t()` 키를
|
|
151
|
+
34. `i18n-layout` — **옛 카탈로그 배치**가 남아 있음 = **경고**(폐지된 루트 `locales/`·앱 평면 `<로케일>.json`) · **오류**(`domain/locales/<로케일>/frontend.json`·제거된 `i18n.dir` 설정). 결정 459 의 정본 배치는 소유자 2개다 — `domain/locales/<로케일>/backend.json`(도메인 공통 서버 문구) · `apps/<앱>/locales/<로케일>/{frontend,backend}.json`(그 앱 화면·서버 문구). 옛 배치를 그냥 두면 **아무도 읽지 않는 폴더**가 되어 전 화면·전 메일이 키 문자열로 degrade 하므로 조용히 두지 않는다. `gaon doctor --fix` 가 이관한다(루트 backend·레거시 단일 파일 → `domain/locales`, 앱 평면 파일 → 그 앱 `frontend.json`, 루트 공용 frontend → **전 앱 복제 후 원본 제거**). 전 앱 복제는 되돌리기 쉬운 방향이라 자동화하고, 잉여 키 정리·도메인 frontend 재배치는 사람이 판단한다 (결정 454·459 · `agents/i18n.md`)
|
|
152
|
+
35. `i18n-app-scope` — 앱 코드가 **그 앱 카탈로그에 없는** 클라 `t()` 키를 참조, 또는 `shared/` 컴포넌트가 쓰는 키가 **일부 앱에만** 존재 = **오류**. 클라 키 유니온은 프로젝트 전체 frontend 합집합이라(두 앱이 같은 `GaonMessages.keys` 를 다른 유니온으로 augment 하면 TS2717 이고 `gaon check` 는 단일 tsc 프로그램이다 — 앱별 유니온이 구조적으로 불가) 타입만으로는 앱 경계를 못 지킨다. 앱 번들에는 그 앱 카탈로그만 실리므로 다른 앱 전용 키는 런타임에 번역 대신 **키 문자열이 그대로** 보인다. `shared/` 는 어느 앱 번들에도 실릴 수 있어 그 키는 **모든 앱**에 있어야 한다(공용 카탈로그는 폐지됐다 — 결정 459 O1 · 복제 + 이 검사로 강제). 문구는 `apps/<앱>/locales/<로케일>/frontend.json` 에 두고 `gaon gen`. 서버 `t()`(`gaonjs/i18n`) 호출은 `i18n-server-scope` 담당이다 (결정 454·459)
|
|
153
|
+
36. `i18n-server-scope` — 서버 `t()`(`gaonjs/i18n`)가 **소유자 경계**를 넘는 키를 참조 = **오류**. `domain/**` 은 `domain/locales` backend 만, `apps/<앱>/**` 은 그 앱 backend ∪ frontend ∪ domain backend 만 볼 수 있다. 이 유형은 두 층이 모두 놓친다 — 서버 키 유니온은 domain ∪ 전 앱이라 **컴파일을 통과**하고(결정 459 O5), 요청 컨텍스트에서는 앱 네임스페이스가 상속돼 **우연히 해석된다**. 같은 코드가 워커(`gaon work`)·크론에서 불리면 앱 스코프가 없어 키가 비므로 "개발 중엔 되는데 잡에서만 키 문자열이 뜬다" 로 샌다. 결정 455 의 2단 계약(빌드=차단 / 런타임=키 렌더+경고)에서 **빌드=차단의 절반이 무너지는** 지점이라 정적으로 못박는다. 수리: 문구를 `domain/locales/<로케일>/backend.json` 으로 올리거나 그 호출을 도메인 밖으로 옮긴다 (결정 459 · `agents/i18n.md` §3)
|
|
153
154
|
|
|
154
155
|
## 3. 로직 배치 One Way 판단표
|
|
155
156
|
|
|
@@ -210,7 +211,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
210
211
|
```bash
|
|
211
212
|
gaon check # .gaon 재생성 → typecheck + vue-tsc + build + doctor (기본 포함 · --no-doctor 로 뺌 · 결정 157)
|
|
212
213
|
gaon test # vitest — DB·NATS 는 실 인프라 (agents/testing.md)
|
|
213
|
-
gaon doctor # 정적 검사
|
|
214
|
+
gaon doctor # 정적 검사 36종 (§2.2)
|
|
214
215
|
```
|
|
215
216
|
|
|
216
217
|
### 4.1 CLI 명령 (전 명령 `--json` 지원)
|
|
@@ -6,39 +6,49 @@
|
|
|
6
6
|
|
|
7
7
|
## 한 장 요약
|
|
8
8
|
|
|
9
|
+
문구의 **소유자**가 파일 위치를 정한다 — 화면이면 **그 앱**, 화면 밖이면 **domain**.
|
|
10
|
+
|
|
9
11
|
| 문구가 어디에 보이나 | 카탈로그 파일 | 부르는 곳 |
|
|
10
12
|
|---|---|---|
|
|
11
|
-
| **화면**(.vue 페이지·컴포넌트·레이아웃) | `locales/<로케일>/frontend.json`
|
|
12
|
-
|
|
|
13
|
+
| **화면**(.vue 페이지·컴포넌트·레이아웃) | `apps/<앱>/locales/<로케일>/frontend.json` **(앱마다 필수)** | **`import { t } from 'gaonjs/vue'`** |
|
|
14
|
+
| 그 앱 **컨트롤러**의 서버 문구(flash·리다이렉트 안내) | `apps/<앱>/locales/<로케일>/backend.json` (선택) | `import { t } from 'gaonjs/i18n'` |
|
|
15
|
+
| 메일·잡·크론·검증 문안·로그 등 **도메인 공통** | `domain/locales/<로케일>/backend.json` | `import { t } from 'gaonjs/i18n'` |
|
|
13
16
|
|
|
14
|
-
한 문장: **"화면에 보이는 문구는 frontend + 클라 `t()`, 화면 밖으로 나가는 문구는
|
|
17
|
+
한 문장: **"화면에 보이는 문구는 그 앱의 frontend + 클라 `t()`, 화면 밖으로 나가는 문구는
|
|
15
18
|
backend + 서버 `t()`."** 텍스트는 카탈로그에 **한 번만** 쓴다 — 컨트롤러가 키를 다시
|
|
16
19
|
열거하거나 타입을 손으로 선언하지 않는다.
|
|
17
20
|
|
|
21
|
+
**루트 공용 `locales/` 는 없다**(결정 459). 여러 앱이 함께 쓰는 UI 문구는 **각 앱에
|
|
22
|
+
같은 키를 둔다**(§10) — doctor 가 전 앱 존재를 강제하므로 손으로 맞출 일은 없다.
|
|
23
|
+
|
|
18
24
|
## 정본 규칙
|
|
19
25
|
|
|
20
|
-
### 1. 카탈로그 저작 레이아웃 (결정
|
|
26
|
+
### 1. 카탈로그 저작 레이아웃 (결정 459)
|
|
21
27
|
|
|
22
28
|
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
backend.json # 서버 전용 — 메일·잡·검증(validation.*)·알림.
|
|
26
|
-
|
|
27
|
-
en/
|
|
28
|
-
backend.json
|
|
29
|
-
frontend.json
|
|
29
|
+
domain/
|
|
30
|
+
locales/
|
|
31
|
+
ko/backend.json # 도메인 공통 서버 전용 — 메일·잡·검증(validation.*)·알림.
|
|
32
|
+
en/backend.json # 화면 문구(frontend.json)는 여기 둘 수 없다 — 부팅 에러.
|
|
30
33
|
apps/
|
|
31
|
-
web/
|
|
32
|
-
|
|
34
|
+
web/
|
|
35
|
+
locales/
|
|
36
|
+
ko/frontend.json # web 화면 문구 — web 번들 청크에만 실린다. (필수)
|
|
37
|
+
ko/backend.json # web 컨트롤러 전용 서버 문구. (선택)
|
|
38
|
+
en/frontend.json
|
|
39
|
+
admin/
|
|
40
|
+
locales/ko/frontend.json …
|
|
33
41
|
```
|
|
34
42
|
|
|
35
|
-
- **키 이름은
|
|
43
|
+
- **키 이름은 위치와 무관하다** — `t('nav.home')` 은 파일이 어디에 있든 그대로다.
|
|
36
44
|
파일 위치가 곧 "누가 이 문구를 볼 수 있나"를 정한다.
|
|
37
|
-
- **backend 는 클라 번들에 물리적으로 없다** — 앱 카탈로그 청크의 입력이
|
|
38
|
-
서버 문구가 `/assets/*` 로
|
|
39
|
-
참조하면 **컴파일 에러**다.
|
|
40
|
-
- **한 키를 두 스코프에 두면 부팅 에러**
|
|
45
|
+
- **backend 는 클라 번들에 물리적으로 없다** — 앱 카탈로그 청크의 입력이 그 앱
|
|
46
|
+
`frontend.json` **하나**뿐이라 서버 문구가 `/assets/*` 로 샐 길이 없다. 타입 축도 같은
|
|
47
|
+
경계를 지켜 클라에서 backend 키를 참조하면 **컴파일 에러**다.
|
|
48
|
+
- **한 키를 두 스코프에 두면 부팅 에러** — 같은 앱의 frontend↔backend 도, 앱↔도메인도
|
|
49
|
+
마찬가지다(어느 번역이 나갈지가 병합 순서·요청 컨텍스트에 좌우되면 안 된다).
|
|
41
50
|
- 카탈로그는 **순수 JSON**(주석·트레일링 콤마 금지). 로케일 등록 = 폴더 존재.
|
|
51
|
+
- **위치는 관례로 고정**이라 `i18n.dir` 설정이 없다(결정 459 — 있으면 doctor 가 error).
|
|
42
52
|
- `gaon.config.ts` 의 `i18n.fallbackLng` 가 **기준 로케일**이다(키 유니온의 출처 · 결정 352).
|
|
43
53
|
|
|
44
54
|
```ts
|
|
@@ -90,7 +100,15 @@ const layouts = import.meta.glob('./layouts/*.vue', { eager: true })
|
|
|
90
100
|
void createGaonApp({ pages, layouts, routes, messages: { catalogs, fallbackLng } })
|
|
91
101
|
```
|
|
92
102
|
|
|
93
|
-
### 3. 서버 문구 = 서버 `t()` (
|
|
103
|
+
### 3. 서버 문구 = 서버 `t()` (결정 459 · 스코프 2개)
|
|
104
|
+
|
|
105
|
+
서버 `t()` 의 해석 범위는 **호출이 일어난 컨텍스트**가 정한다:
|
|
106
|
+
|
|
107
|
+
| 어디서 불렀나 | 보이는 카탈로그 | 우선순위 |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `domain/**`(mails·jobs·services·listeners·schedule) | domain-backend **만** | — (단일) |
|
|
110
|
+
| `apps/<앱>/**`(컨트롤러 등 서버 코드) | 그 앱 backend ∪ 그 앱 frontend ∪ domain-backend | **앱 먼저**, 없으면 domain |
|
|
111
|
+
| 워커(`gaon work`)·크론·부팅 스크립트 | domain-backend **만**(앱 컨텍스트가 없다) | — (단일) |
|
|
94
112
|
|
|
95
113
|
```ts
|
|
96
114
|
// domain/mails/welcome.ts — 수신자 로케일로 렌더된다(결정 160)
|
|
@@ -116,28 +134,47 @@ export const SendDigest = job(async ({ locale }: { locale: string }) => {
|
|
|
116
134
|
|
|
117
135
|
| 표면 | 시그니처 | 쓰는 곳 |
|
|
118
136
|
|---|---|---|
|
|
119
|
-
| 번역 | `t(key, params?)` | 요청 로케일(ALS) 자동 추종 |
|
|
137
|
+
| 번역 | `t(key, params?)` | 요청 로케일(ALS) + 앱 스코프 자동 추종 |
|
|
120
138
|
| 현재 언어 | `currentLanguage(): string` | 요청 로케일 |
|
|
139
|
+
| 현재 앱 스코프 | `currentApp(): string \| undefined` | 진단용(요청 밖이면 undefined) |
|
|
121
140
|
| 지원 언어 | `languages(): readonly string[]` | 설정된 supportedLngs |
|
|
122
141
|
| 로케일 고정 | `runWithLanguage(lng, fn)` | 잡·크론·스크립트(요청 밖) |
|
|
123
142
|
| 고정 번역기 | `translator(lng)` | 테스트·비요청 경로 |
|
|
124
143
|
|
|
125
|
-
서버 `t()`
|
|
126
|
-
문구가 필요할 수 있다). 반대 방향만 막힌다 —
|
|
144
|
+
앱 스코프에서는 서버 `t()` 가 그 앱의 backend ∪ frontend 를 **모두** 볼 수 있다(SSR·로그·
|
|
145
|
+
관리 스크립트에서 화면 문구가 필요할 수 있다 · 결정 459 O5). 반대 방향만 막힌다 —
|
|
146
|
+
클라는 frontend 만.
|
|
127
147
|
|
|
128
|
-
|
|
148
|
+
**⚠️ 도메인 코드는 앱 키에 기대면 안 된다.** 앱 요청이 부른 도메인 서비스는 같은 요청
|
|
149
|
+
컨텍스트라 앱 네임스페이스가 **우연히 보인다**. 그런데 같은 코드가 워커·크론에서 불리면
|
|
150
|
+
앱 스코프가 없어 그 키가 빈다 — "개발 중엔 되는데 잡에서만 키 문자열이 뜬다" 는 가장 나쁜
|
|
151
|
+
형태다. `gaon doctor` 의 `i18n-server-scope` 가 정적으로 막는다(§9).
|
|
152
|
+
|
|
153
|
+
### 4. 키·파라미터 타입 — `.gaon/messages.d.ts` 두 갈래 (결정 454 · 459)
|
|
129
154
|
|
|
130
155
|
`gaon gen`/`dev`/`check` 가 카탈로그를 읽어 **두 유니온**을 생성한다:
|
|
131
|
-
서버(`gaonjs/i18n`) = backend ∪
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
t('
|
|
156
|
+
서버(`gaonjs/i18n`) = domain-backend ∪ 전 앱 backend ∪ 전 앱 frontend /
|
|
157
|
+
클라(`gaonjs/vue`) = 전 앱 frontend 만.
|
|
158
|
+
|
|
159
|
+
```vue expect-error
|
|
160
|
+
<!-- apps/web/pages/Posts/Bad.vue — 아래 세 줄은 **전부 컴파일 에러**다 -->
|
|
161
|
+
<script setup lang="ts">
|
|
162
|
+
import { t } from 'gaonjs/vue'
|
|
163
|
+
const a = t('mail.welcome.subject', { name: '가온' }) // ❌ 클라에서 backend 키
|
|
164
|
+
const b = t('greeting') // ❌ {{name}} 보간 파라미터 누락
|
|
165
|
+
const c = t('greeting', { nam: '가온' }) // ❌ 파라미터 이름 오타
|
|
166
|
+
</script>
|
|
139
167
|
```
|
|
140
168
|
|
|
169
|
+
**유니온은 앱별로 못 나눈다** — 두 앱이 `GaonMessages.keys` 를 서로 다른 유니온으로
|
|
170
|
+
augment 하면 **TS2717**(Subsequent property declarations must have the same type)이고,
|
|
171
|
+
`gaon check` 는 단일 tsc 프로그램이라 앱마다 다른 프로그램을 돌릴 수 없다(결정 454 실측 ·
|
|
172
|
+
결정 459 §5.1 로 유효). **파일은 앱별인데 타입은 프로젝트 전역**인 이 비대칭이 함정이다 —
|
|
173
|
+
그래서 앱 경계는 ① 번들 물리 경계 ② doctor 가 강제한다(§9).
|
|
174
|
+
|
|
175
|
+
산출은 **루트 `.gaon/messages.d.ts` 하나**다(앱별로 쪼개면 "앱별 타입" 이라는 오해를
|
|
176
|
+
만든다 · 결정 459 O7). 앱별 `.gaon/` 에는 **값 모듈**(`messages.catalog.ts`)만 나간다.
|
|
177
|
+
|
|
141
178
|
카탈로그가 없거나 frontend 스코프가 없으면 키가 `string` 으로 열린다(무회귀). 이 파일은
|
|
142
179
|
자동 생성이니 직접 수정하지 않는다.
|
|
143
180
|
|
|
@@ -148,11 +185,11 @@ t('cart.items', { count: 'many' }) // ❌ count 는 number — 컴파일 에
|
|
|
148
185
|
**같은 문자열**을 낸다(85 케이스 동치 게이트로 고정).
|
|
149
186
|
|
|
150
187
|
```json
|
|
151
|
-
// locales/en/frontend.json — 영어는 단수/복수 구분
|
|
188
|
+
// apps/web/locales/en/frontend.json — 영어는 단수/복수 구분
|
|
152
189
|
{ "cart": { "items_one": "{{count}} item", "items_other": "{{count}} items" } }
|
|
153
190
|
```
|
|
154
191
|
```json
|
|
155
|
-
// locales/ko/frontend.json — 한국어는 _other 만
|
|
192
|
+
// apps/web/locales/ko/frontend.json — 한국어는 _other 만
|
|
156
193
|
{ "cart": { "items_other": "상품 {{count}}개" } }
|
|
157
194
|
```
|
|
158
195
|
|
|
@@ -202,27 +239,49 @@ t(key as never) // 없으면 'posts.abc' 가 화면에
|
|
|
202
239
|
녹는다(이 계약이 막는 것이 정확히 그 침묵이다).
|
|
203
240
|
- 런타임까지 **실패로 만들고 싶다면**(엄격 모드) 지금은 지원하지 않는다 — 필요하면 요청하라.
|
|
204
241
|
|
|
205
|
-
### 9.
|
|
242
|
+
### 9. 정적 검사 3종 (`gaon doctor`)
|
|
243
|
+
|
|
244
|
+
| 검사 | 무엇을 잡나 | 등급 |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `locale-parity` | 같은 스코프 안에서 **로케일 간** 키 부분 누락(§9.1) | 경고 |
|
|
247
|
+
| `i18n-app-scope` | 클라 `t()` 가 **그 앱에 없는** 키 참조 · shared 키가 일부 앱에만 존재 | 오류 |
|
|
248
|
+
| `i18n-server-scope` | 서버 `t()` 가 **소유자 경계**를 넘는 키 참조(도메인 → 앱 키 등) | 오류 |
|
|
249
|
+
|
|
250
|
+
#### 9.1 로케일 커버리지 — `locale-parity` (결정 216 · 456 · 459)
|
|
206
251
|
|
|
207
252
|
키 유니온은 기준 로케일 하나에서 나오므로, 어떤 키를 `ko` 에만 넣고 `ja` 에 빠뜨리면
|
|
208
|
-
컴파일은 통과하고 일본어 사용자만 조용히 fallback 을 본다.
|
|
209
|
-
|
|
253
|
+
컴파일은 통과하고 일본어 사용자만 조용히 fallback(서버) 또는 키 문자열(클라)을 본다.
|
|
254
|
+
비교 단위는 **3종**이다: ① domain-backend ② 앱별 frontend ③ 앱별 backend.
|
|
255
|
+
**스코프·소유자를 가로지르는 비교는 하지 않는다** — 앱 전용 키가 전량 "도메인에 없음"
|
|
256
|
+
으로 뜨는 오탐이 되기 때문이다. 복수형 접미사는 base 로 정규화해 비교한다(en `_one`/`_other`
|
|
257
|
+
와 ko `_other` 의 정당한 차이를 누락으로 세지 않는다).
|
|
258
|
+
|
|
259
|
+
### 10. 여러 앱이 함께 쓰는 문구 · `shared/` 컴포넌트 (결정 459 O1)
|
|
210
260
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
261
|
+
공용 카탈로그가 없으므로, 여러 앱이 쓰는 UI 원자 문구(`ui.close`·`ui.prev`·`ui.save` …)는
|
|
262
|
+
**각 앱의 `frontend.json` 에 같은 키를 둔다**. 복제가 비용처럼 보이지만 대안이 더 나쁘다 —
|
|
263
|
+
문구를 props 로 끌어올리면(`closeLabel`·`prevLabel`) 원자 컴포넌트마다 prop 이 생기고
|
|
264
|
+
사용처마다 문구를 다시 적어야 해서, 결정 454 가 없앤 "선언 지점 3곳" 통증이 되살아난다.
|
|
265
|
+
|
|
266
|
+
**`shared/` 컴포넌트가 쓰는 키는 모든 앱에 있어야 한다** — shared 는 어느 앱 번들에도
|
|
267
|
+
실릴 수 있어서, 키가 한 앱에만 있으면 **다른 앱 화면에서만** 키 문자열이 뜨는 조용한
|
|
268
|
+
실패가 된다. `i18n-app-scope` 가 누락된 앱 이름을 짚어 준다:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
shared 컴포넌트가 쓰는 'ui.close' 가 일부 앱 카탈로그에 없습니다(누락: admin)
|
|
272
|
+
→ apps/admin/locales/<로케일>/frontend.json 에 같은 키를 추가하고 gaon gen
|
|
273
|
+
```
|
|
215
274
|
|
|
216
275
|
## 정본 예시
|
|
217
276
|
|
|
218
|
-
### (a) 새 화면 문구 추가 — 앱 전용
|
|
277
|
+
### (a) 새 화면 문구 추가 — 그 앱 전용
|
|
219
278
|
|
|
220
|
-
```
|
|
221
|
-
// apps/web/locales/ko.json
|
|
279
|
+
```json
|
|
280
|
+
// apps/web/locales/ko/frontend.json
|
|
222
281
|
{ "posts": { "heading": "게시글 목록", "empty": "아직 글이 없습니다." } }
|
|
223
282
|
```
|
|
224
|
-
```
|
|
225
|
-
// apps/web/locales/en.json ← 같은 키를 나란히(locale-parity)
|
|
283
|
+
```json
|
|
284
|
+
// apps/web/locales/en/frontend.json ← 같은 키를 나란히(locale-parity)
|
|
226
285
|
{ "posts": { "heading": "Posts", "empty": "No posts yet." } }
|
|
227
286
|
```
|
|
228
287
|
```vue fragment
|
|
@@ -235,10 +294,14 @@ import { t } from 'gaonjs/vue'
|
|
|
235
294
|
gaon gen # .gaon/messages.d.ts(두 갈래) + apps/web/.gaon/messages.catalog.ts 재생성
|
|
236
295
|
```
|
|
237
296
|
|
|
238
|
-
### (b)
|
|
297
|
+
### (b) 여러 앱이 쓰는 chrome 문구 — 각 앱에 복제
|
|
239
298
|
|
|
240
|
-
```
|
|
241
|
-
// locales/ko/frontend.json
|
|
299
|
+
```json
|
|
300
|
+
// apps/web/locales/ko/frontend.json
|
|
301
|
+
{ "nav": { "home": "홈", "posts": "게시판" } }
|
|
302
|
+
```
|
|
303
|
+
```json
|
|
304
|
+
// apps/admin/locales/ko/frontend.json ← 같은 키를 admin 에도(전 앱 존재를 doctor 가 강제)
|
|
242
305
|
{ "nav": { "home": "홈", "posts": "게시판" } }
|
|
243
306
|
```
|
|
244
307
|
```vue
|
|
@@ -254,33 +317,64 @@ import { t, Link } from 'gaonjs/vue'
|
|
|
254
317
|
</template>
|
|
255
318
|
```
|
|
256
319
|
|
|
257
|
-
### (c) 서버
|
|
320
|
+
### (c) 도메인 공통 서버 문구(메일·검증)
|
|
258
321
|
|
|
259
|
-
```
|
|
260
|
-
// locales/ko/backend.json
|
|
261
|
-
{
|
|
322
|
+
```json
|
|
323
|
+
// domain/locales/ko/backend.json
|
|
324
|
+
{
|
|
325
|
+
"mail": { "welcome": { "subject": "{{name}}님, 환영합니다" } },
|
|
326
|
+
"validation": { "required": "필수 입력 항목입니다." }
|
|
327
|
+
}
|
|
262
328
|
```
|
|
263
329
|
```ts fragment
|
|
264
330
|
import { t } from 'gaonjs/i18n'
|
|
265
331
|
const subject = t('mail.welcome.subject', { name: user.name })
|
|
266
332
|
```
|
|
267
333
|
|
|
268
|
-
### (d)
|
|
334
|
+
### (d) 그 앱 컨트롤러 전용 서버 문구
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
// apps/web/locales/ko/backend.json
|
|
338
|
+
{ "flash": { "saved": "저장했습니다." } }
|
|
339
|
+
```
|
|
340
|
+
```ts controller-action
|
|
341
|
+
async update() {
|
|
342
|
+
this.flash('notice', t('flash.saved')) // web 스코프에서만 보이는 키
|
|
343
|
+
return this.redirect('/posts')
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### (e) 옛 배치에서 이관 (결정 459 · breaking)
|
|
269
348
|
|
|
270
349
|
```bash
|
|
271
|
-
gaon doctor # i18n-layout
|
|
272
|
-
gaon doctor --fix
|
|
273
|
-
#
|
|
274
|
-
|
|
350
|
+
gaon doctor # i18n-layout — 루트 locales/ · 앱 평면 파일 · i18n.dir 을 전부 지목
|
|
351
|
+
gaon doctor --fix --yes
|
|
352
|
+
# locales/<로케일>/backend.json → domain/locales/<로케일>/backend.json (이동)
|
|
353
|
+
# locales/<로케일>.json → domain/locales/<로케일>/backend.json (이동 · 미분류는 서버 전용)
|
|
354
|
+
# apps/<앱>/locales/<로케일>.json → apps/<앱>/locales/<로케일>/frontend.json (이동)
|
|
355
|
+
# locales/<로케일>/frontend.json → 전 앱 frontend.json 에 복제 후 원본 제거
|
|
356
|
+
# 그 뒤 각 앱이 안 쓰는 키는 사람이 지운다(전 앱 복제는 되돌리기 쉬운 방향의 자동화)
|
|
357
|
+
gaon gen && gaon check
|
|
275
358
|
```
|
|
276
359
|
|
|
277
360
|
## 알려진 함정
|
|
278
361
|
|
|
362
|
+
- **루트 `locales/` 는 더 이상 읽히지 않는다**(결정 459) — 그대로 두면 전 화면·전 메일이
|
|
363
|
+
키 문자열로 degrade 한다. `gaon doctor --fix` 로 이관한다.
|
|
364
|
+
- **도메인 코드에서 앱 키를 쓰지 말 것** — 요청 안에서는 우연히 해석되고 워커·크론에서만
|
|
365
|
+
빈다. `i18n-server-scope` 가 잡는다(§3 ⚠️).
|
|
366
|
+
- **`shared/` 가 쓰는 키는 전 앱에 둘 것** — 한 앱에만 있으면 다른 앱에서만 키가 뜬다(§10).
|
|
279
367
|
- **클라에서 backend 키를 쓰지 말 것** — 컴파일 에러로 막힌다. 그 문구가 화면에도 필요하면
|
|
280
368
|
`frontend.json` 으로 **옮긴다**(양쪽에 복사하면 부팅 에러).
|
|
281
369
|
- **다른 앱 전용 키를 쓰지 말 것** — 타입은 프로젝트 전체 frontend 합집합이라 통과하지만
|
|
282
370
|
(앱별 유니온은 TS2717 로 구조적으로 불가), 그 앱 번들에는 없어서 화면에 키 문자열이
|
|
283
|
-
그대로 뜬다. `
|
|
371
|
+
그대로 뜬다. `i18n-app-scope` 가 잡는다.
|
|
372
|
+
- **파일은 앱별인데 타입은 전역이다** — 카탈로그를 앱 폴더로 나눠도 키 유니온은 프로젝트
|
|
373
|
+
전체다(§4). "앱을 나눴으니 타입도 갈렸겠지" 가 이 배치의 가장 큰 오해다.
|
|
374
|
+
- **`domain/locales/<로케일>/frontend.json` 은 만들 수 없다** — 도메인은 화면을 소유하지
|
|
375
|
+
않는다. 부팅에서 실패하고 doctor 가 error 로 짚는다.
|
|
376
|
+
- **`i18n.dir` 은 제거됐다**(결정 459) — 위치가 관례로 고정이라 설정이 없다. 남아 있으면
|
|
377
|
+
doctor error(효과 없는 설정을 조용히 두면 "설정한 폴더를 읽겠지" 라는 오해가 된다).
|
|
284
378
|
- **`.vue` 에서 서버 `t`(`gaonjs/i18n`)를 import 하지 말 것** — ALS 기반 서버 전용이다.
|
|
285
379
|
클라는 `gaonjs/vue` 의 `t`.
|
|
286
380
|
- **`v-html` + `t()` + 사용자 입력 조합 금지** — 템플릿 텍스트 보간(`{{ t(...) }}`)은 Vue 가
|
|
@@ -292,28 +386,29 @@ gaon gen
|
|
|
292
386
|
로케일인지 짚어 준다.
|
|
293
387
|
- **잡·크론은 로케일을 페이로드에 싣는다** — 요청 밖이라 ALS 가 없다(`runWithLanguage`).
|
|
294
388
|
- **메일은 요청 로케일이 아니라 수신자 로케일** — `deliver(data, { locale })`(결정 160).
|
|
295
|
-
- **`
|
|
296
|
-
|
|
389
|
+
- **`fallbackLng` 는 문자열 리터럴로**(결정 412) — 변수·env 표현식은 타입 축과 doctor 가
|
|
390
|
+
정적으로 못 읽는다.
|
|
297
391
|
- **`supportedLngs: []`(빈 배열)은 "미지정" 과 같다**(결정 414).
|
|
298
|
-
- **`gaon check` 가 부팅 조건을 먼저 본다**(결정 413) — 카탈로그가
|
|
299
|
-
가 지목한 카탈로그가 없으면 check 가 실패한다.
|
|
300
|
-
- **검증 실패 문안은 backend** — 예약 namespace `validation.<code>` 를
|
|
301
|
-
두면 서버가 요청 로케일로 번역한다(결정 183).
|
|
392
|
+
- **`gaon check` 가 부팅 조건을 먼저 본다**(결정 413) — 카탈로그가 하나도 없거나
|
|
393
|
+
`fallbackLng` 가 지목한 카탈로그가 없으면 check 가 실패한다.
|
|
394
|
+
- **검증 실패 문안은 도메인 backend** — 예약 namespace `validation.<code>` 를
|
|
395
|
+
`domain/locales/<로케일>/backend.json` 에 두면 서버가 요청 로케일로 번역한다(결정 183).
|
|
302
396
|
|
|
303
397
|
## 관련 결정 번호
|
|
304
398
|
|
|
305
399
|
| 결정 | 요지 |
|
|
306
400
|
|---|---|
|
|
307
|
-
| §7 (v0.15) | i18n 배터리 ·
|
|
401
|
+
| §7 (v0.15) | i18n 배터리 · 카탈로그 · t() |
|
|
308
402
|
| 결정 158 (13차 W2) | `.gaon/messages.d.ts` 키 타입 브리지 — 없는 키 컴파일 에러 |
|
|
309
403
|
| 결정 159 (13차 W1) | 요청별 로케일 자동 협상 · `this.setLocale` · detect 설정 |
|
|
310
404
|
| 결정 181 | 복수형 base 키 노출(`t('cart.items', { count })`) |
|
|
311
405
|
| 결정 213 | (구) Vue 소비 = 서버 주도 render props/sharedProps 만 — **결정 454 로 부분 번복**(§6 탈출구로 존속) |
|
|
312
406
|
| 결정 214 (13차 W2) | 최초 문서 셸 `<html lang>` 이 협상 로케일 자동 추종 |
|
|
313
|
-
| 결정 216 (13차 W4) | `locale-parity` 경고 — 결정 454 로 **스코프별 비교** |
|
|
314
|
-
| 결정 352 | 기준 로케일 = `fallbackLng`
|
|
407
|
+
| 결정 216 (13차 W4) | `locale-parity` 경고 — 결정 454·459 로 **스코프별 비교** |
|
|
408
|
+
| 결정 352 | 기준 로케일 = `fallbackLng` |
|
|
315
409
|
| 결정 353 · 413 | 빈 카탈로그·지목 로케일 부재 = 부팅 fail-loud + check 사전 검사 |
|
|
316
410
|
| 결정 412 · 414 | i18n config 정적 분석 범위·경고 · 생성 키 이스케이프 · `supportedLngs: []` |
|
|
317
|
-
|
|
|
318
|
-
|
|
|
319
|
-
|
|
|
411
|
+
| 결정 455 | 런타임 없는키 = **키 렌더 + 경고**(서버·클라 동일) · 요청/화면을 죽이지 않는다 · 정적 키는 컴파일에서 차단(2단) |
|
|
412
|
+
| 결정 454 | 클라 `t()`(`gaonjs/vue`) · backend/frontend 스코프 분리 · 두 갈래 타입 · 앱별 카탈로그 청크(immutable·seal 면제) · 공유 prop `locale` |
|
|
413
|
+
| 결정 456 | `locale-parity` 앱 스코프 확장 — 앱 내부에서 비교(루트 교차 비교는 오탐이라 기각) |
|
|
414
|
+
| **결정 459** | **소유자 2개로 재편**(결정 454 부분 반전) — 루트 공용 `locales/` **폐지** · 화면 문구 = `apps/<앱>/locales/<로케일>/frontend.json`(필수) · 앱 서버 문구 = 같은 폴더 `backend.json`(선택) · 도메인 공통 = `domain/locales/<로케일>/backend.json` · 서버 `t()` 는 `app:<앱>` → `domain` 네임스페이스 폴백(앱 ALS) · 소유자 간 키 충돌 = 부팅 fail-loud · `i18n.dir` 제거 · `domain/locales/*/frontend.json` = error · shared 키는 전 앱 복제 + doctor 강제 · 신규 검사 `i18n-server-scope` · 타입 유니온은 **여전히 프로젝트 전역**(TS2717) |
|
|
@@ -312,8 +312,9 @@ const form = useForm({ title: '', body: '' })
|
|
|
312
312
|
#### 검증 사유 로케일화 — 예약 namespace `validation.*` (결정 183)
|
|
313
313
|
|
|
314
314
|
검증 실패의 **필드별 사유**(`form.errors.<필드>` 로 엔드유저에 노출되는 부분)는 요청
|
|
315
|
-
로케일로 번역된다 — `i18n` 이 설정돼 있고
|
|
316
|
-
`validation.<code>`** 로 번역을
|
|
315
|
+
로케일로 번역된다 — `i18n` 이 설정돼 있고 프로젝트가 `domain/locales/<로케일>/backend.json`
|
|
316
|
+
의 **예약 namespace `validation.<code>`** 로 번역을 제공하면(결정 459 — 검증 문안은 화면
|
|
317
|
+
밖 문구라 도메인이 소유한다). 프레임웍은 **안정적 코드만** 노출하고 번역은
|
|
317
318
|
앱 몫이다(The One Way: 코드는 프레임웍, 문안은 앱). 키가 없으면 내장 fallback(한국어)로
|
|
318
319
|
떨어져 거동이 보존된다. i18n 미설정이면 항상 fallback.
|
|
319
320
|
|
|
@@ -330,7 +331,7 @@ const form = useForm({ title: '', body: '' })
|
|
|
330
331
|
| `invalid` | — | 그 외 coerce 예외(비-CoerceError) |
|
|
331
332
|
|
|
332
333
|
```json
|
|
333
|
-
// locales/en.json —
|
|
334
|
+
// domain/locales/en/backend.json — 검증 문안을 로케일별로 준다(i18next {{max}} 보간).
|
|
334
335
|
{ "validation": {
|
|
335
336
|
"required": "This field is required.",
|
|
336
337
|
"too_long": "At most {{max}} characters (got {{len}})."
|
|
@@ -54,11 +54,12 @@ export default defineConfig({
|
|
|
54
54
|
}
|
|
55
55
|
: undefined,
|
|
56
56
|
|
|
57
|
-
// 다국어(§7 · 결정
|
|
58
|
-
// locales/<로케일>/frontend.json — 화면 문구. 클라 t()(gaonjs/vue)
|
|
59
|
-
// locales/<로케일>/backend.json —
|
|
60
|
-
//
|
|
61
|
-
// 요청 로케일은 세션>쿠키>Accept-Language 로
|
|
57
|
+
// 다국어(§7 · 결정 459). 문구의 **소유자**가 위치를 정한다 — 화면이면 그 앱, 화면 밖이면 domain:
|
|
58
|
+
// apps/<앱>/locales/<로케일>/frontend.json — 그 앱 화면 문구. 클라 t()(gaonjs/vue) · 그 앱 청크로만 나간다.
|
|
59
|
+
// apps/<앱>/locales/<로케일>/backend.json — 그 앱 컨트롤러 전용 서버 문구(선택).
|
|
60
|
+
// domain/locales/<로케일>/backend.json — 메일·잡·검증 등 도메인 공통 **서버 전용**. 클라로 나가지 않는다.
|
|
61
|
+
// 위치는 관례로 고정이라 dir 설정이 없다. 요청 로케일은 세션>쿠키>Accept-Language 로
|
|
62
|
+
// 자동 협상된다(결정 159 · this.setLocale 로 전환).
|
|
62
63
|
i18n: { fallbackLng: 'ko', supportedLngs: ['ko', 'en'] },
|
|
63
64
|
|
|
64
65
|
// 웹 서버 리슨 옵션. --port · env PORT 로 덮을 수 있다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
33
|
"typescript": "^5.9.0",
|
|
34
34
|
"vite": "^7.0.0",
|
|
35
|
-
"@gaonjs/data": "0.26.2",
|
|
36
|
-
"@gaonjs/i18n": "0.4.1",
|
|
37
|
-
"@gaonjs/config": "0.25.12",
|
|
38
35
|
"@gaonjs/async": "0.22.0",
|
|
39
|
-
"@gaonjs/
|
|
36
|
+
"@gaonjs/config": "0.26.0",
|
|
40
37
|
"@gaonjs/core": "0.3.0",
|
|
41
|
-
"@gaonjs/
|
|
38
|
+
"@gaonjs/i18n": "0.5.0",
|
|
39
|
+
"@gaonjs/mail": "0.5.4",
|
|
40
|
+
"@gaonjs/web": "0.33.0",
|
|
41
|
+
"@gaonjs/data": "0.26.2"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"const fs=require('fs');fs.cpSync('src/templates','dist/templates',{recursive:true,filter:(s)=>!s.endsWith('.ts')});fs.rmSync('dist/templates/index.ts',{force:true})\""
|
|
44
|
+
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"const fs=require('fs');fs.rmSync('dist/templates/project',{recursive:true,force:true});fs.cpSync('src/templates','dist/templates',{recursive:true,filter:(s)=>!s.endsWith('.ts')});fs.rmSync('dist/templates/index.ts',{force:true})\""
|
|
45
45
|
}
|
|
46
46
|
}
|
|
File without changes
|
|
File without changes
|