@gaonjs/cli 0.61.0 → 0.62.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.
Files changed (31) hide show
  1. package/dist/commands/check.js +8 -3
  2. package/dist/dev.d.ts +17 -4
  3. package/dist/dev.js +10 -3
  4. package/dist/doctor/fixers/i18n-layout.d.ts +7 -0
  5. package/dist/doctor/fixers/i18n-layout.js +40 -0
  6. package/dist/doctor/fixers/index.d.ts +1 -0
  7. package/dist/doctor/fixers/index.js +13 -0
  8. package/dist/doctor/i18n-app-scope.d.ts +8 -0
  9. package/dist/doctor/i18n-app-scope.js +178 -0
  10. package/dist/doctor/i18n-layout.d.ts +4 -0
  11. package/dist/doctor/i18n-layout.js +47 -0
  12. package/dist/doctor/locale-parity.js +21 -6
  13. package/dist/doctor/types.d.ts +1 -1
  14. package/dist/doctor.d.ts +3 -1
  15. package/dist/doctor.js +13 -1
  16. package/dist/index.js +2 -2
  17. package/dist/messages-gen.d.ts +28 -4
  18. package/dist/messages-gen.js +137 -17
  19. package/dist/templates/project/AGENTS.md.tpl +4 -2
  20. package/dist/templates/project/agents/frontend.md.tpl +8 -4
  21. package/dist/templates/project/agents/i18n.md.tpl +187 -213
  22. package/dist/templates/project/apps/web/locales/en.json.tpl +3 -0
  23. package/dist/templates/project/apps/web/locales/ko.json.tpl +3 -0
  24. package/dist/templates/project/apps/web/main.ts.tpl +7 -0
  25. package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +3 -3
  26. package/dist/templates/project/gaon.config.ts.tpl +7 -0
  27. package/dist/templates/project/locales/en/backend.json.tpl +4 -0
  28. package/dist/templates/project/locales/en/frontend.json.tpl +8 -0
  29. package/dist/templates/project/locales/ko/backend.json.tpl +4 -0
  30. package/dist/templates/project/locales/ko/frontend.json.tpl +7 -0
  31. package/package.json +5 -5
@@ -1,6 +1,30 @@
1
+ import { type LocaleResources } from '@gaonjs/i18n';
2
+ /** 메시지 축이 카탈로그 모듈을 낼 앱(결정 454). dev 레이아웃이 만들어 넘긴다. */
3
+ export interface MessagesApp {
4
+ readonly name: string;
5
+ /** apps/<앱>/locales (앱 전용 frontend 카탈로그 · 없어도 된다). */
6
+ readonly localesDir: string;
7
+ /** apps/<앱>/.gaon/messages.catalog.ts */
8
+ readonly catalogOut: string;
9
+ }
1
10
  /**
2
- * locales/ 카탈로그에서 .gaon/messages.d.ts 생성한다. 카탈로그가 없거나 비어
3
- * 있으면 생성하지 않는다(GaonMessages 를 비운 채로 둬 t() 키가 string 폴백 — i18n 을
4
- * 안 쓰는 프로젝트가 never 로 깨지지 않게). 생성 여부를 돌려준다.
11
+ * locales/ 카탈로그에서 `.gaon/messages.d.ts`(두 갈래)와 앱별 카탈로그 모듈을 생성한다.
12
+ * 카탈로그가 없거나 비면 생성하지 않는다(GaonMessages 를 비운 채로 둬 키가 string 폴백 —
13
+ * i18n 을 안 쓰는 프로젝트가 never 로 깨지지 않게). 생성 여부를 돌려준다.
5
14
  */
6
- export declare function generateMessagesDts(localesDir: string, out: string, baseLng?: string): boolean;
15
+ export declare function generateMessagesDts(localesDir: string, out: string, baseLng?: string, apps?: readonly MessagesApp[]): boolean;
16
+ interface CatalogInput {
17
+ readonly localesDir: string;
18
+ readonly app: MessagesApp;
19
+ readonly appLocales: LocaleResources;
20
+ readonly frontendLangs: readonly string[];
21
+ readonly fallbackLng: string;
22
+ }
23
+ /**
24
+ * 결정 454: 앱별 카탈로그 값 모듈을 쓴다. 로케일별 로더가 **공용 frontend + 앱 카탈로그**를
25
+ * 동적 import 해 vite 가 로케일별 해시 청크(.js)로 쪼갠다 — `/assets/*` immutable 캐시 대상이며
26
+ * `text/javascript` 라 seal 봉인 대상(application/json)에서 자연 면제된다.
27
+ * 생성 여부를 돌려준다(프론트 카탈로그가 없으면 false + 스테일 제거).
28
+ */
29
+ export declare function generateMessagesCatalog(input: CatalogInput): boolean;
30
+ export {};
@@ -1,35 +1,155 @@
1
- // @gaonjs/cli · .gaon/messages.d.ts 생성기 (결정 158 · 13차 W2)
1
+ // @gaonjs/cli · .gaon 메시지 축 생성기 (결정 158 · 13차 W2 · 결정 454)
2
2
  //
3
- // tables.d.ts·routes.d.ts 와 같은 .gaon 파이프라인의 번째 축(메시지). locales/
4
- // 카탈로그의 키를 유니온 타입으로 물성화해, t('key') 의 존재하지 않는 키를 컴파일
5
- // 타임에 잡는다(현재는 GaonMessages 비어 있어 키가 string 으로 열림). 생성 파일은
6
- // 타입만 담는다(규칙 3). @gaonjs/i18n 공개 API(loadLocales·renderMessagesDts)만 쓴다.
3
+ // tables.d.ts·routes.d.ts 와 같은 .gaon 파이프라인의 메시지 축. 결정 454 부터 두 산출을 낸다:
4
+ //
5
+ // 1) `.gaon/messages.d.ts` **두 갈래 타입 브리지**
6
+ // · `gaonjs/i18n`(서버) = backend frontend ∪ 앱 frontend
7
+ // · `gaonjs/vue`(클라) = frontend 만(+ 키별 보간 파라미터) → 서버 전용 문구를
8
+ // 클라에서 참조하면 컴파일 에러.
9
+ // 2) `apps/<앱>/.gaon/messages.catalog.ts` — **앱별 값 모듈**(routes.manifest.ts 선례).
10
+ // 그 앱의 frontend 카탈로그만 동적 import 로 묶어 vite 가 로케일별 청크로 쪼갠다.
11
+ // backend.json 은 입력에 아예 없다(노출 차단은 번들 경계가 물리적으로 강제).
12
+ //
13
+ // @gaonjs/i18n 의 공개 API(loadScopedLocales·loadAppLocales·renderMessagesDtsSplit 등)만 쓴다.
7
14
  import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
8
- import { dirname } from 'node:path';
9
- import { loadLocales, renderMessagesDts } from '@gaonjs/i18n';
15
+ import { dirname, join, relative, sep } from 'node:path';
16
+ import { extractParams, flattenKeys, loadAppLocales, loadScopedLocales, renderMessagesDtsSplit, withPluralBaseKeys, } from '@gaonjs/i18n';
17
+ /** 번역 트리 두 개를 깊은 병합한다(리프 충돌은 로더가 이미 fail-loud). */
18
+ function mergeTrees(a, b) {
19
+ const out = { ...a };
20
+ for (const [k, v] of Object.entries(b)) {
21
+ const cur = out[k];
22
+ const isTree = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
23
+ out[k] = isTree(cur) && isTree(v) ? mergeTrees(cur, v) : v;
24
+ }
25
+ return out;
26
+ }
27
+ function translationOf(res, lng) {
28
+ if (lng === undefined)
29
+ return {};
30
+ return res[lng]?.translation ?? {};
31
+ }
32
+ /** 기준 로케일 = config i18n.fallbackLng(결정 352). 없으면 정렬 첫 로케일. */
33
+ function pickBaseLng(langs, baseLng) {
34
+ if (baseLng && langs.includes(baseLng))
35
+ return baseLng;
36
+ return [...langs].sort()[0];
37
+ }
10
38
  /**
11
- * locales/ 카탈로그에서 .gaon/messages.d.ts 생성한다. 카탈로그가 없거나 비어
12
- * 있으면 생성하지 않는다(GaonMessages 를 비운 채로 둬 t() 키가 string 폴백 — i18n 을
13
- * 안 쓰는 프로젝트가 never 로 깨지지 않게). 생성 여부를 돌려준다.
39
+ * locales/ 카탈로그에서 `.gaon/messages.d.ts`(두 갈래)와 앱별 카탈로그 모듈을 생성한다.
40
+ * 카탈로그가 없거나 비면 생성하지 않는다(GaonMessages 를 비운 채로 둬 키가 string 폴백 —
41
+ * i18n 을 안 쓰는 프로젝트가 never 로 깨지지 않게). 생성 여부를 돌려준다.
14
42
  */
15
- export function generateMessagesDts(localesDir, out, baseLng) {
43
+ export function generateMessagesDts(localesDir, out, baseLng, apps = []) {
16
44
  // 결정 414: 카탈로그가 사라졌으면 **옛 생성물을 지운다** — 남겨두면 없어진 키의
17
45
  // 유니온이 그대로 살아 t('없어진키') 가 계속 컴파일된다(제거를 못 잡는 사각).
18
46
  if (!existsSync(localesDir)) {
19
47
  if (existsSync(out))
20
48
  rmSync(out, { force: true });
49
+ for (const app of apps)
50
+ removeStale(app.catalogOut);
21
51
  return false;
22
52
  }
23
- const resources = loadLocales(localesDir);
24
- if (Object.keys(resources).length === 0) {
53
+ const scoped = loadScopedLocales(localesDir);
54
+ const appResources = new Map();
55
+ for (const app of apps)
56
+ appResources.set(app.name, loadAppLocales(app.localesDir));
57
+ const langs = new Set([...Object.keys(scoped.backend), ...Object.keys(scoped.frontend)]);
58
+ for (const res of appResources.values())
59
+ for (const lng of Object.keys(res))
60
+ langs.add(lng);
61
+ if (langs.size === 0) {
25
62
  if (existsSync(out))
26
63
  rmSync(out, { force: true });
64
+ for (const app of apps)
65
+ removeStale(app.catalogOut);
27
66
  return false;
28
67
  }
68
+ const base = pickBaseLng([...langs], baseLng);
69
+ // 클라 키 = 공용 frontend ∪ 모든 앱 frontend(결정 454 · 앱×앱 축은 타입으로 못 나눈다 —
70
+ // TS2717. 앱 경계는 청크 분리와 doctor `i18n-app-scope` 가 강제한다).
71
+ let clientTree = translationOf(scoped.frontend, base);
72
+ for (const res of appResources.values())
73
+ clientTree = mergeTrees(clientTree, translationOf(res, base));
74
+ const serverTree = mergeTrees(translationOf(scoped.backend, base), clientTree);
75
+ const clientKeys = withPluralBaseKeys(flattenKeys(clientTree));
76
+ const serverKeys = withPluralBaseKeys(flattenKeys(serverTree));
29
77
  mkdirSync(dirname(out), { recursive: true });
30
- // 결정 352: 기준 로케일 = config i18n.fallbackLng(호출자가 정적 분석으로 전달).
31
- // 이전엔 항상 알파벳순 첫 로케일이라 컴파일 보증이 fallback 체인과 다른 로케일에
32
- // 정박했다(en/ko + fallbackLng:'ko' 프로젝트에서 기준이 en).
33
- writeFileSync(out, renderMessagesDts(resources, baseLng), 'utf8');
78
+ writeFileSync(out, renderMessagesDtsSplit({
79
+ baseLng: base,
80
+ serverKeys,
81
+ clientKeys,
82
+ clientParams: extractParams(clientTree),
83
+ }), 'utf8');
84
+ // 앱별 카탈로그 값 모듈 — frontend 소스가 하나도 없으면 만들지 않는다(레거시 레이아웃).
85
+ const fallback = base ?? 'en';
86
+ for (const app of apps) {
87
+ generateMessagesCatalog({
88
+ localesDir,
89
+ app,
90
+ appLocales: appResources.get(app.name) ?? {},
91
+ frontendLangs: Object.keys(scoped.frontend),
92
+ fallbackLng: fallback,
93
+ });
94
+ }
95
+ return true;
96
+ }
97
+ function removeStale(path) {
98
+ if (existsSync(path))
99
+ rmSync(path, { force: true });
100
+ }
101
+ /** import 지정자로 쓸 상대 경로(항상 ./ 또는 ../ 로 시작 · POSIX 구분자). */
102
+ function importPath(fromDir, target) {
103
+ const rel = relative(fromDir, target).split(sep).join('/');
104
+ return rel.startsWith('.') ? rel : `./${rel}`;
105
+ }
106
+ /**
107
+ * 결정 454: 앱별 카탈로그 값 모듈을 쓴다. 로케일별 로더가 **공용 frontend + 앱 카탈로그**를
108
+ * 동적 import 해 vite 가 로케일별 해시 청크(.js)로 쪼갠다 — `/assets/*` immutable 캐시 대상이며
109
+ * `text/javascript` 라 seal 봉인 대상(application/json)에서 자연 면제된다.
110
+ * 생성 여부를 돌려준다(프론트 카탈로그가 없으면 false + 스테일 제거).
111
+ */
112
+ export function generateMessagesCatalog(input) {
113
+ const outDir = dirname(input.app.catalogOut);
114
+ const langs = [...new Set([...input.frontendLangs, ...Object.keys(input.appLocales)])].sort();
115
+ if (langs.length === 0) {
116
+ removeStale(input.app.catalogOut);
117
+ return false;
118
+ }
119
+ const lines = [];
120
+ lines.push('// AUTO-GENERATED by @gaonjs/cli — 편집 금지.');
121
+ lines.push('// `gaon check` / `gaon dev` / `gaon gen` 가 이 파일을 재생성한다(결정 454).');
122
+ lines.push('// 클라이언트 번역 카탈로그: 로케일 → 로더(공용 frontend + 이 앱의 카탈로그).');
123
+ lines.push('// 서버 전용 문구(locales/<로케일>/backend.json)는 여기에 실리지 않는다 — 클라로 나가지 않는다.');
124
+ lines.push("import type { CatalogLoaders } from 'gaonjs/vue'");
125
+ lines.push('');
126
+ lines.push('export const catalogs: CatalogLoaders = {');
127
+ for (const lng of langs) {
128
+ const sources = [];
129
+ const shared = join(input.localesDir, lng, 'frontend.json');
130
+ if (existsSync(shared))
131
+ sources.push(importPath(outDir, shared));
132
+ const appFile = join(input.app.localesDir, `${lng}.json`);
133
+ if (input.appLocales[lng] !== undefined && existsSync(appFile))
134
+ sources.push(importPath(outDir, appFile));
135
+ if (sources.length === 0)
136
+ continue;
137
+ const body = sources.length === 1
138
+ ? `import('${sources[0]}')`
139
+ : `Promise.all([${sources.map((s) => `import('${s}')`).join(', ')}])`;
140
+ lines.push(` '${lng}': () => ${body},`);
141
+ }
142
+ lines.push('}');
143
+ lines.push('');
144
+ lines.push(`export const fallbackLng = '${input.fallbackLng}'`);
145
+ lines.push('');
146
+ const text = lines.join('\n');
147
+ // 로더가 하나도 없으면(전부 backend 뿐) 모듈을 만들지 않는다 — 빈 카탈로그 배선은 오해를 만든다.
148
+ if (!text.includes('() =>')) {
149
+ removeStale(input.app.catalogOut);
150
+ return false;
151
+ }
152
+ mkdirSync(outDir, { recursive: true });
153
+ writeFileSync(input.app.catalogOut, text, 'utf8');
34
154
  return true;
35
155
  }
@@ -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` 검사 33
116
+ ### 2.2 `gaon doctor` 검사 35
117
117
 
118
118
  1. `response-mixing` — 한 액션 안 render/JSON/redirect 혼용 (E-3)
119
119
  2. `n-plus-one` — include 미사용 · loop 안 관계 호출 (E-4)
@@ -148,6 +148,8 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
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` — 카탈로그가 **레거시 단일 파일**(`locales/<로케일>.json`) = **경고**. 결정 454 의 정본 저작 레이아웃은 언어별 폴더 + 스코프 분리(`locales/<로케일>/{backend,frontend}.json`)다. 단일 파일은 계속 동작하지만 미분류라 **전량 서버 전용**으로 취급돼 클라 `t()`(`gaonjs/vue`)가 그 문구를 쓸 수 없다(클라 키 축 미생성). `gaon doctor --fix` 가 `locales/<로케일>/backend.json` 으로 옮겨 주고(내용·키 무변경), 그 뒤 화면 문구만 `frontend.json` 으로 나누면 된다 — 자동 분류는 하지 않는다(frontend 로 잘못 보내면 클라 번들·CDN 캐시에 노출돼 되돌리기 어렵다) (결정 454 · `agents/i18n.md`)
152
+ 35. `i18n-app-scope` — 앱 코드가 **그 앱 카탈로그에 없는** 클라 `t()` 키를 참조 = **오류**. 클라 키 유니온은 프로젝트 전체 frontend 합집합이라(두 앱이 같은 `GaonMessages.keys` 를 다른 유니온으로 augment 하면 TS2717 이고 `gaon check` 는 단일 tsc 프로그램이다 — 앱별 유니온이 구조적으로 불가) 타입만으로는 앱 경계를 못 지킨다. 앱 번들에는 그 앱 카탈로그만 실리므로 다른 앱 전용 키는 런타임에 번역 대신 **키 문자열이 그대로** 보인다. 공용으로 쓸 문구는 `locales/<로케일>/frontend.json`, 그 앱 전용이면 `apps/<앱>/locales/<로케일>.json` 에 두고 `gaon gen`. 서버 `t()`(`gaonjs/i18n`) 호출은 대상이 아니다 (결정 454)
151
153
 
152
154
  ## 3. 로직 배치 One Way 판단표
153
155
 
@@ -208,7 +210,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
208
210
  ```bash
209
211
  gaon check # .gaon 재생성 → typecheck + vue-tsc + build + doctor (기본 포함 · --no-doctor 로 뺌 · 결정 157)
210
212
  gaon test # vitest — DB·NATS 는 실 인프라 (agents/testing.md)
211
- gaon doctor # 정적 검사 33종 (§2.2)
213
+ gaon doctor # 정적 검사 35종 (§2.2)
212
214
  ```
213
215
 
214
216
  ### 4.1 CLI 명령 (전 명령 `--json` 지원)
@@ -50,9 +50,12 @@ const props = pageProps<'web:posts#index'>()
50
50
  // <template> 에서 shared.currentUser?.name · shared.csrf · shared.flash.success
51
51
  // 앱이 app.config sharedProps 로 등록한 키(locale·theme 등)도 같은 자리에서 읽힌다(결정 150).
52
52
  ```
53
- **i18n 문구도 경로로 온다** — Vue 는 `t()` 직접 부르지 않는다(서버 ALS 전용).
54
- 페이지 문구는 컨트롤러 render props, nav·레이아웃 같은 chrome 문구는 `sharedProps`
55
- 서버가 번역해 흘려보낸다(`agents/i18n.md` §5 · 결정 213).
53
+ **i18n 문구는 여기가 아니라 클라 `t()` 온다**(결정 454) `import { t } from 'gaonjs/vue'`
54
+ `.vue` 에서 직접 부른다. 카탈로그는 `locales/<로케일>/frontend.json`(공용)과
55
+ `apps/<앱>/locales/<로케일>.json`(앱 전용)이고, 서버 전용 문구(`backend.json`)를 클라에서
56
+ 참조하면 컴파일 에러다(`agents/i18n.md`). 컨트롤러 render props·`sharedProps` 로 번역을
57
+ 흘려보내던 옛 경로(결정 213)는 탈출구로만 남는다 — `sharedProps` 는 번역과 무관한 앱 공유
58
+ 값(테마·플래그)에 쓴다.
56
59
  `pageProps<K>()` 반환에도 교차되어 `props.csrf` 로도 읽히지만, 라우트 키가 필요 없는
57
60
  `useShared()` 가 정본 표면이다(임의 라우트 키를 빌려 currentUser 를 읽던 우회 트릭을 없앤다).
58
61
  - **구조분해 금지 — `pageProps` 와 같은 함정** (`packages/vue/src/shared.ts`). `useShared()`
@@ -560,7 +563,8 @@ async function runSearch(q: string) {
560
563
  | 결정 128 | `useChannel` 자동 재연결(지수 백오프 1s·2s·5s·10s·지터 · `onReconnect` 로 놓친 데이터 따라잡기 · 미인가 4401 은 재연결 안 함 · `agents/realtime.md` §4) |
561
564
  | 결정 198 | 클라 환경변수 접근자 `env`(gaonjs/vue · `.vue` 의 import.meta.env TS1470 회피) · VITE_* 접두만 노출·접두 제거 · `.gaon/env.d.ts`(.env 스캔) 타입 브리지 · doctor no-import-meta-env(§9) |
562
565
  | 결정 206 | UI 킷 §8 슬롯·props 요약표(카탈로그가 이름만이라 소스 열람 유발 · O-2 해소) · named slot 비대칭 명시(PageHeader `#actions` 복수 vs EmptyState `#action` 단수) |
563
- | 결정 213 | i18n Vue 소비 = 서버 주도 render props/sharedProps 만 · `t()`·`useT()` 클라 미노출(`agents/i18n.md` §5) |
566
+ | 결정 213 | (구) i18n Vue 소비 = 서버 주도 render props/sharedProps 만 **결정 454 로 부분 번복**(탈출구로 존속) |
567
+ | 결정 454 | 클라 `t()` 신설(`gaonjs/vue`) · 카탈로그 backend/frontend 분리 · 두 갈래 키 타입 · 앱별 카탈로그 청크(`agents/i18n.md`) |
564
568
  | 결정 217 | doctor `shared-purity`(구 shared-composable-purity 개명) — `shared/` 의 .ts 컴포저블 + .vue 컴포넌트 순수성(pageProps/api 호출·domain 값 import 금지 · §4) |
565
569
  | 결정 271 | W4 표면 정합 — `Head` 재수출(`gaonjs/vue` · `<Head title>` 제목 조합자 발화) 외 표면/최적화 4건(§12 결정 271) |
566
570
  | 결정 299 | 타입 브리지 PropsOf 정정 — 유니온 분배(조건부 redirect 혼합 액션의 never 붕괴 봉합) + `this.json(data)` 언랩(`{json,status}` 래퍼 타입 거짓 봉합 · `JsonResult<T>` 제네릭) (§2) |