@spfn/cms 0.1.0-alpha.9 → 0.2.0-beta.2

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 (171) hide show
  1. package/README.md +320 -359
  2. package/dist/actions.d.ts +12 -6
  3. package/dist/actions.js +25 -10
  4. package/dist/actions.js.map +1 -1
  5. package/dist/config.d.ts +39 -0
  6. package/dist/config.js +39 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/errors.d.ts +149 -0
  9. package/dist/errors.js +164 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +138 -20
  12. package/dist/index.js +212 -23
  13. package/dist/index.js.map +1 -1
  14. package/dist/server.d.ts +44 -81
  15. package/dist/server.js +610 -256
  16. package/dist/server.js.map +1 -1
  17. package/migrations/0000_medical_ozymandias.sql +54 -0
  18. package/migrations/meta/0000_snapshot.json +336 -0
  19. package/migrations/meta/_journal.json +13 -0
  20. package/package.json +54 -44
  21. package/dist/actions.d.ts.map +0 -1
  22. package/dist/client.d.ts +0 -138
  23. package/dist/client.d.ts.map +0 -1
  24. package/dist/client.js +0 -62
  25. package/dist/client.js.map +0 -1
  26. package/dist/cms.config.d.ts +0 -77
  27. package/dist/cms.config.d.ts.map +0 -1
  28. package/dist/cms.config.js +0 -111
  29. package/dist/cms.config.js.map +0 -1
  30. package/dist/entities/cms-audit-logs.d.ts +0 -213
  31. package/dist/entities/cms-audit-logs.d.ts.map +0 -1
  32. package/dist/entities/cms-audit-logs.js +0 -103
  33. package/dist/entities/cms-audit-logs.js.map +0 -1
  34. package/dist/entities/cms-draft-cache.d.ts +0 -188
  35. package/dist/entities/cms-draft-cache.d.ts.map +0 -1
  36. package/dist/entities/cms-draft-cache.js +0 -112
  37. package/dist/entities/cms-draft-cache.js.map +0 -1
  38. package/dist/entities/cms-label-values.d.ts +0 -192
  39. package/dist/entities/cms-label-values.d.ts.map +0 -1
  40. package/dist/entities/cms-label-values.js +0 -105
  41. package/dist/entities/cms-label-values.js.map +0 -1
  42. package/dist/entities/cms-label-versions.d.ts +0 -207
  43. package/dist/entities/cms-label-versions.d.ts.map +0 -1
  44. package/dist/entities/cms-label-versions.js +0 -80
  45. package/dist/entities/cms-label-versions.js.map +0 -1
  46. package/dist/entities/cms-labels.d.ts +0 -189
  47. package/dist/entities/cms-labels.d.ts.map +0 -1
  48. package/dist/entities/cms-labels.js +0 -48
  49. package/dist/entities/cms-labels.js.map +0 -1
  50. package/dist/entities/cms-published-cache.d.ts +0 -199
  51. package/dist/entities/cms-published-cache.d.ts.map +0 -1
  52. package/dist/entities/cms-published-cache.js +0 -103
  53. package/dist/entities/cms-published-cache.js.map +0 -1
  54. package/dist/entities/index.d.ts +0 -10
  55. package/dist/entities/index.d.ts.map +0 -1
  56. package/dist/entities/index.js +0 -10
  57. package/dist/entities/index.js.map +0 -1
  58. package/dist/generators/index.d.ts +0 -19
  59. package/dist/generators/index.d.ts.map +0 -1
  60. package/dist/generators/index.js +0 -19
  61. package/dist/generators/index.js.map +0 -1
  62. package/dist/generators/label-sync-generator.d.ts +0 -33
  63. package/dist/generators/label-sync-generator.d.ts.map +0 -1
  64. package/dist/generators/label-sync-generator.js +0 -86
  65. package/dist/generators/label-sync-generator.js.map +0 -1
  66. package/dist/helpers/locale.actions.d.ts +0 -132
  67. package/dist/helpers/locale.actions.d.ts.map +0 -1
  68. package/dist/helpers/locale.actions.js +0 -210
  69. package/dist/helpers/locale.actions.js.map +0 -1
  70. package/dist/helpers/locale.constants.d.ts +0 -10
  71. package/dist/helpers/locale.constants.d.ts.map +0 -1
  72. package/dist/helpers/locale.constants.js +0 -10
  73. package/dist/helpers/locale.constants.js.map +0 -1
  74. package/dist/helpers/locale.d.ts +0 -17
  75. package/dist/helpers/locale.d.ts.map +0 -1
  76. package/dist/helpers/locale.js +0 -20
  77. package/dist/helpers/locale.js.map +0 -1
  78. package/dist/helpers/sync.d.ts +0 -41
  79. package/dist/helpers/sync.d.ts.map +0 -1
  80. package/dist/helpers/sync.js +0 -309
  81. package/dist/helpers/sync.js.map +0 -1
  82. package/dist/index.d.ts.map +0 -1
  83. package/dist/init.d.ts +0 -31
  84. package/dist/init.d.ts.map +0 -1
  85. package/dist/init.js +0 -36
  86. package/dist/init.js.map +0 -1
  87. package/dist/labels/helpers.d.ts +0 -31
  88. package/dist/labels/helpers.d.ts.map +0 -1
  89. package/dist/labels/helpers.js +0 -60
  90. package/dist/labels/helpers.js.map +0 -1
  91. package/dist/labels/index.d.ts +0 -7
  92. package/dist/labels/index.d.ts.map +0 -1
  93. package/dist/labels/index.js +0 -7
  94. package/dist/labels/index.js.map +0 -1
  95. package/dist/repositories/cms-draft-cache.repository.d.ts +0 -62
  96. package/dist/repositories/cms-draft-cache.repository.d.ts.map +0 -1
  97. package/dist/repositories/cms-draft-cache.repository.js +0 -56
  98. package/dist/repositories/cms-draft-cache.repository.js.map +0 -1
  99. package/dist/repositories/cms-label-values.repository.d.ts +0 -32
  100. package/dist/repositories/cms-label-values.repository.d.ts.map +0 -1
  101. package/dist/repositories/cms-label-values.repository.js +0 -72
  102. package/dist/repositories/cms-label-values.repository.js.map +0 -1
  103. package/dist/repositories/cms-labels.repository.d.ts +0 -53
  104. package/dist/repositories/cms-labels.repository.d.ts.map +0 -1
  105. package/dist/repositories/cms-labels.repository.js +0 -77
  106. package/dist/repositories/cms-labels.repository.js.map +0 -1
  107. package/dist/repositories/cms-published-cache.repository.d.ts +0 -53
  108. package/dist/repositories/cms-published-cache.repository.d.ts.map +0 -1
  109. package/dist/repositories/cms-published-cache.repository.js +0 -54
  110. package/dist/repositories/cms-published-cache.repository.js.map +0 -1
  111. package/dist/repositories/index.d.ts +0 -8
  112. package/dist/repositories/index.d.ts.map +0 -1
  113. package/dist/repositories/index.js +0 -9
  114. package/dist/repositories/index.js.map +0 -1
  115. package/dist/routes/labels/[id]/contract.d.ts +0 -68
  116. package/dist/routes/labels/[id]/contract.d.ts.map +0 -1
  117. package/dist/routes/labels/[id]/contract.js +0 -84
  118. package/dist/routes/labels/[id]/contract.js.map +0 -1
  119. package/dist/routes/labels/[id]/index.d.ts +0 -10
  120. package/dist/routes/labels/[id]/index.d.ts.map +0 -1
  121. package/dist/routes/labels/[id]/index.js +0 -96
  122. package/dist/routes/labels/[id]/index.js.map +0 -1
  123. package/dist/routes/labels/by-key/[key]/contract.d.ts +0 -24
  124. package/dist/routes/labels/by-key/[key]/contract.d.ts.map +0 -1
  125. package/dist/routes/labels/by-key/[key]/contract.js +0 -28
  126. package/dist/routes/labels/by-key/[key]/contract.js.map +0 -1
  127. package/dist/routes/labels/by-key/[key]/index.d.ts +0 -8
  128. package/dist/routes/labels/by-key/[key]/index.d.ts.map +0 -1
  129. package/dist/routes/labels/by-key/[key]/index.js +0 -32
  130. package/dist/routes/labels/by-key/[key]/index.js.map +0 -1
  131. package/dist/routes/labels/contract.d.ts +0 -59
  132. package/dist/routes/labels/contract.d.ts.map +0 -1
  133. package/dist/routes/labels/contract.js +0 -75
  134. package/dist/routes/labels/contract.js.map +0 -1
  135. package/dist/routes/labels/index.d.ts +0 -10
  136. package/dist/routes/labels/index.d.ts.map +0 -1
  137. package/dist/routes/labels/index.js +0 -73
  138. package/dist/routes/labels/index.js.map +0 -1
  139. package/dist/routes/published-cache/contract.d.ts +0 -25
  140. package/dist/routes/published-cache/contract.d.ts.map +0 -1
  141. package/dist/routes/published-cache/contract.js +0 -35
  142. package/dist/routes/published-cache/contract.js.map +0 -1
  143. package/dist/routes/published-cache/index.d.ts +0 -8
  144. package/dist/routes/published-cache/index.d.ts.map +0 -1
  145. package/dist/routes/published-cache/index.js +0 -33
  146. package/dist/routes/published-cache/index.js.map +0 -1
  147. package/dist/routes/values/[labelId]/[version]/contract.d.ts +0 -29
  148. package/dist/routes/values/[labelId]/[version]/contract.d.ts.map +0 -1
  149. package/dist/routes/values/[labelId]/[version]/contract.js +0 -33
  150. package/dist/routes/values/[labelId]/[version]/contract.js.map +0 -1
  151. package/dist/routes/values/[labelId]/[version]/index.d.ts +0 -8
  152. package/dist/routes/values/[labelId]/[version]/index.d.ts.map +0 -1
  153. package/dist/routes/values/[labelId]/[version]/index.js +0 -45
  154. package/dist/routes/values/[labelId]/[version]/index.js.map +0 -1
  155. package/dist/routes/values/[labelId]/contract.d.ts +0 -38
  156. package/dist/routes/values/[labelId]/contract.d.ts.map +0 -1
  157. package/dist/routes/values/[labelId]/contract.js +0 -59
  158. package/dist/routes/values/[labelId]/contract.js.map +0 -1
  159. package/dist/routes/values/[labelId]/index.d.ts +0 -8
  160. package/dist/routes/values/[labelId]/index.d.ts.map +0 -1
  161. package/dist/routes/values/[labelId]/index.js +0 -42
  162. package/dist/routes/values/[labelId]/index.js.map +0 -1
  163. package/dist/server.d.ts.map +0 -1
  164. package/dist/store.d.ts +0 -87
  165. package/dist/store.d.ts.map +0 -1
  166. package/dist/store.js +0 -205
  167. package/dist/store.js.map +0 -1
  168. package/dist/types.d.ts +0 -74
  169. package/dist/types.d.ts.map +0 -1
  170. package/dist/types.js +0 -7
  171. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -1,490 +1,451 @@
1
1
  # @spfn/cms
2
2
 
3
- Content Management System for Next.js with JSON-based labels and automatic database synchronization.
3
+ Type-safe Content Management System for Next.js with automatic database synchronization and published cache.
4
4
 
5
5
  ## Features
6
6
 
7
- - 📁 **JSON file-based labels** - Simple file structure for label management
8
- - 🔄 **Auto-sync to database** on server startup and during development
9
- - 🌐 **Multi-language support** (i18n)
10
- - 🍪 **Cookie-based locale management** - Automatic locale detection and persistence
11
- - 📦 **Folder-based structure** for better organization
12
- - 🔥 **Hot reload** during development
13
- - 💾 **Published cache** for optimal performance
14
- - **Server Actions** for client-side locale management
15
- - 🛠️ **Built on Drizzle ORM**
7
+ - 🎯 **Type-safe labels** - Full TypeScript support with autocomplete
8
+ - 🔄 **Auto-sync** - Database synchronization on server startup
9
+ - 🌐 **Multi-language** - Type-checked locale support
10
+ - 🍪 **Smart locale detection** - Cookie-based with automatic fallback
11
+ - 💾 **Published cache** - 17x faster queries (5ms vs 87ms)
12
+ - 🎨 **Nested structure** - Organize labels hierarchically
13
+ - 🔧 **Template variables** - Dynamic content with `{placeholder}` syntax
14
+ - 📝 **Draft & versioning** - Version control and audit logs
16
15
 
17
16
  ## Installation
18
17
 
19
- ### Recommended: Using SPFN CLI (Automatic Database Setup)
20
-
21
18
  ```bash
22
19
  pnpm spfn add @spfn/cms
23
20
  ```
24
21
 
25
- This command will:
26
- 1. ✅ Install the package
27
- 2. ✅ Discover CMS database schemas automatically
28
- 3. ✅ Generate migrations for 6 CMS tables
29
- 4. ✅ Apply migrations to your database
30
- 5. ✅ Show setup guide
31
-
32
- **Tables created:**
33
- - `cms_labels` - Label definitions (10 columns, 2 indexes)
34
- - `cms_label_values` - Label values per locale (7 columns, 2 indexes, 1 FK)
35
- - `cms_label_versions` - Version history (9 columns, 2 indexes, 1 FK)
36
- - `cms_draft_cache` - Draft content cache (6 columns, 2 indexes)
37
- - `cms_published_cache` - Published content cache (7 columns, 1 index)
38
- - `cms_audit_logs` - Change audit trail (8 columns, 4 indexes, 1 FK)
22
+ ## Quick Start
39
23
 
40
- ### Manual Installation
24
+ ### 1. Define Labels & Configuration
41
25
 
42
- ```bash
43
- pnpm add @spfn/cms
44
- ```
26
+ ```typescript
27
+ // labels.ts
28
+ import { defineLabelConfig, defineLabels, createCmsClient } from '@spfn/cms';
29
+
30
+ // Configure locales
31
+ export const labelConfig = defineLabelConfig({
32
+ locales: ['en', 'ko'] as const,
33
+ defaultLocale: 'ko',
34
+ fallbackLocale: 'en'
35
+ });
45
36
 
46
- Then run database migrations:
37
+ // Define labels with nested structure
38
+ export const labelsDefinition = defineLabels({
39
+ home: {
40
+ hero: {
41
+ title: { en: "Welcome", ko: "환영합니다" },
42
+ subtitle: { en: "Start your journey", ko: "여정을 시작하세요" },
43
+ greeting: { en: "Hello {name}!", ko: "{name}님 안녕하세요!" }
44
+ },
45
+ cta: { en: "Get Started", ko: "시작하기" }
46
+ },
47
+ about: {
48
+ title: { en: "About Us", ko: "회사 소개" }
49
+ }
50
+ });
47
51
 
48
- ```bash
49
- pnpm spfn db generate # Generate migrations
50
- pnpm spfn db migrate # Apply migrations
52
+ // Create client with API, getLabel, getLabels, and format
53
+ export const { api, getLabel, getLabels, format } = createCmsClient(
54
+ labelsDefinition,
55
+ labelConfig
56
+ );
51
57
  ```
52
58
 
53
- **Note:** Manual installation requires that you have `DATABASE_URL` configured in your `.env.local` file.
59
+ ### 2. Enable Auto-Sync
54
60
 
55
- ## Quick Start
61
+ ```typescript
62
+ // server.config.ts
63
+ import { defineServerConfig } from '@spfn/core/server';
64
+ import { syncLabels } from '@spfn/cms/server';
65
+ import { labelsDefinition } from './labels';
56
66
 
57
- ### 1. Create Label Files
58
67
 
59
- Create JSON files organized by sections and categories:
68
+ // Option 1: Single definition
69
+ export default defineServerConfig()
70
+ .lifecycle({
71
+ afterInfrastructure: async () => {
72
+ await syncLabels(labelsDefinition);
73
+ }
74
+ })
75
+ .build();
60
76
 
61
- ```
62
- src/cms/labels/
63
- layout/ ← Section name
64
- nav.json ← Category
65
- footer.json
66
- home/
67
- hero.json
68
- features.json
69
- ```
77
+ // Option 2: Multiple definitions (organized in separate files)
78
+ import { homeLabels } from './labels/home';
79
+ import { aboutLabels } from './labels/about';
70
80
 
71
- **Example:** `src/cms/labels/layout/nav.json`
72
-
73
- ```json
74
- {
75
- "about": {
76
- "key": "layout.nav.about",
77
- "defaultValue": "About",
78
- "description": "Navigation link for About page"
79
- },
80
- "services": {
81
- "key": "layout.nav.services",
82
- "defaultValue": "Services",
83
- "description": "Navigation link for Services page"
84
- },
85
- "team": {
86
- "key": "layout.nav.team",
87
- "defaultValue": "Team"
88
- }
89
- }
81
+ export default defineServerConfig()
82
+ .lifecycle({
83
+ afterInfrastructure: async () => {
84
+ await syncLabels([homeLabels, aboutLabels]);
85
+ }
86
+ })
87
+ .build();
90
88
  ```
91
89
 
92
- **Multi-language example:** `src/cms/labels/home/hero.json`
90
+ ### 3. Use in Your App
93
91
 
94
- ```json
95
- {
96
- "title": {
97
- "key": "home.hero.title",
98
- "defaultValue": {
99
- "ko": "혁신적인 솔루션",
100
- "en": "Innovative Solutions"
101
- }
102
- },
103
- "subtitle": {
104
- "key": "home.hero.subtitle",
105
- "defaultValue": {
106
- "ko": "비즈니스 성장을 위한 최고의 파트너",
107
- "en": "Your Best Partner for Business Growth"
108
- }
109
- }
110
- }
111
- ```
92
+ **Server Component (Single Section):**
112
93
 
113
- **Variable substitution:** `src/cms/labels/layout/footer.json`
94
+ ```typescript
95
+ import { getLabel, format } from '@/labels';
114
96
 
115
- ```json
116
- {
117
- "copyright": {
118
- "key": "layout.footer.copyright",
119
- "defaultValue": "© {year} Company. All rights reserved."
120
- }
97
+ export default async function HomePage() {
98
+ // Single section - direct access without section name
99
+ const label = await getLabel('home');
100
+
101
+ return (
102
+ <div>
103
+ <h1>{label.hero.title}</h1>
104
+ <p>{label.hero.subtitle}</p>
105
+ <button>{label.cta}</button>
106
+
107
+ {/* Template variables */}
108
+ <p>{format(label.hero.greeting, { name: 'John' })}</p>
109
+ {/* Output: "John님 안녕하세요!" */}
110
+ </div>
111
+ );
121
112
  }
122
113
  ```
123
114
 
124
- ### 2. Enable Auto-Sync on Server Startup
125
-
126
- Configure `src/server/server.config.ts`:
115
+ **Server Component (Multiple Sections):**
127
116
 
128
117
  ```typescript
129
- import type { ServerConfig } from '@spfn/core/server';
130
- import { initLabelSync } from '@spfn/cms';
131
-
132
- export default {
133
- beforeRoutes: async (app) => {
134
- await initLabelSync({
135
- verbose: true,
136
- labelsDir: 'src/cms/labels' // Optional, this is the default
137
- });
138
- },
139
- } satisfies ServerConfig;
118
+ import { getLabels } from '@/labels';
119
+
120
+ export default async function MultiSectionPage() {
121
+ // Multiple sections - with section names as keys
122
+ const labels = await getLabels(['home', 'about']);
123
+
124
+ return (
125
+ <div>
126
+ <h1>{labels.home.hero.title}</h1>
127
+ <p>{labels.about.title}</p>
128
+ </div>
129
+ );
130
+ }
140
131
  ```
141
132
 
142
- ### 3. Enable Auto-Sync During Development
133
+ **Locale Management:**
143
134
 
144
- Your `.spfnrc.json` should include:
135
+ ```typescript
136
+ 'use client';
137
+ import { setLocale } from '@spfn/cms/actions';
145
138
 
146
- ```json
147
- {
148
- "codegen": {
149
- "generators": [
150
- {
151
- "name": "@spfn/cms:label-sync",
152
- "enabled": true
153
- }
154
- ]
155
- }
139
+ export function LanguageSwitcher() {
140
+ return (
141
+ <div>
142
+ <button onClick={() => setLocale('ko')}>한국어</button>
143
+ <button onClick={() => setLocale('en')}>English</button>
144
+ </div>
145
+ );
156
146
  }
157
147
  ```
158
148
 
159
- This is automatically configured when you run `pnpm spfn add @spfn/cms`.
149
+ ## Key Features
160
150
 
161
- ### 4. Use Labels in Your App
151
+ ### 🎯 Type Safety
162
152
 
163
- **Server Component:**
153
+ **Section Name Validation:**
164
154
 
165
155
  ```typescript
166
- import { getSection } from '@spfn/cms/server';
167
-
168
- export default async function HomePage() {
169
- const { t } = await getSection('layout', 'ko');
170
-
171
- return <h1>{t('nav.team')}</h1>;
172
- }
173
- ```
156
+ // Valid section names are enforced at compile time
157
+ const label = await getLabel('home'); // OK
158
+ await getLabel('homee'); // Compile error: 'homee' is not a valid section
174
159
 
175
- **With variable substitution:**
160
+ // getLabel returns direct access (no section wrapper)
161
+ const homeLabel = await getLabel('home');
162
+ homeLabel.hero.title; // OK - direct access
163
+ homeLabel.cta; // OK
176
164
 
177
- ```typescript
178
- const { t } = await getSection('layout');
179
- const copyright = t('footer.copyright', undefined, {
180
- year: new Date().getFullYear()
181
- });
182
- // → "© 2025 Company. All rights reserved."
165
+ // ✅ getLabels requires array and returns sections with names
166
+ const labels = await getLabels(['home', 'about']);
167
+ labels.home.hero.title; // OK
168
+ labels.about.title; // OK
169
+ labels.contact.email; // ❌ Compile error: 'contact' was not requested
183
170
  ```
184
171
 
185
- **Client Component:**
172
+ **Property Access Validation:**
186
173
 
187
174
  ```typescript
188
- 'use client';
189
- import { useSection } from '@spfn/cms/client';
175
+ // Single section - direct access
176
+ const label = await getLabel('signup');
190
177
 
191
- export default function Nav() {
192
- const { t, loading } = useSection('layout', { autoLoad: true });
178
+ // IDE autocomplete works perfectly
179
+ label.title; // OK - direct access
180
+ label.userName; // OK
181
+ label.email; // OK
193
182
 
194
- if (loading) return <div>Loading...</div>;
183
+ // Typos are caught at compile time
184
+ label.titlee; // ❌ Compile error
185
+ label.userName; // ❌ Compile error (typo)
195
186
 
196
- return (
197
- <nav>
198
- <a>{t('nav.about')}</a>
199
- <a>{t('nav.services')}</a>
200
- </nav>
201
- );
202
- }
187
+ // Multiple sections - with section names
188
+ const labels = await getLabels(['home', 'about']);
189
+ labels.home.hero.title; // OK
190
+ labels.about.title; // OK
203
191
  ```
204
192
 
205
- ## File Structure
193
+ **API Distinction:**
206
194
 
207
- ```
208
- src/cms/labels/
209
- layout/ # Section: layout
210
- nav.json # Category: nav
211
- footer.json # Category: footer
212
- home/ # Section: home
213
- hero.json # Category: hero
214
- features.json # Category: features
215
- ```
216
-
217
- **How it maps:**
218
- - Folder name = Section name
219
- - JSON file name = Category name (for organization only)
220
- - Inside JSON: `key` field defines the actual label key
195
+ ```typescript
196
+ // getLabel: Single section, direct access
197
+ const signup = await getLabel('signup');
198
+ signup.title; // ✅ Direct access (cleaner!)
221
199
 
222
- Example:
223
- ```
224
- src/cms/labels/layout/nav.json:
225
- key: "layout.nav.team" t('nav.team') in code
200
+ // getLabels: Multiple sections, section names as keys
201
+ const multi = await getLabels(['home', 'signup']);
202
+ multi.home.title; // ✅ With section name
203
+ multi.signup.userName; // With section name
226
204
  ```
227
205
 
228
- ## JSON Label Format
206
+ ### 🎨 Nested Structure
229
207
 
230
208
  ```typescript
231
- {
232
- "labelName": {
233
- "key": "section.category.name", // Required: Unique identifier
234
- "defaultValue": "Text" | {...}, // Required: String or i18n object
235
- "description": "Optional description" // Optional: For documentation
236
- }
237
- }
238
- ```
209
+ defineLabels({
210
+ features: {
211
+ analytics: {
212
+ title: { en: "Analytics", ko: "분석" },
213
+ description: { en: "Track metrics", ko: "지표 추적" }
214
+ },
215
+ security: {
216
+ title: { en: "Security", ko: "보안" }
217
+ }
218
+ }
219
+ });
239
220
 
240
- **Single language:**
241
- ```json
242
- {
243
- "welcome": {
244
- "key": "home.welcome",
245
- "defaultValue": "Welcome"
246
- }
247
- }
221
+ // Single section - direct access with nesting
222
+ const label = await getLabel('features');
223
+ label.analytics.title; // "분석" (auto locale, direct access!)
224
+ label.analytics.description; // "지표 추적"
225
+ label.security.title; // "보안"
248
226
  ```
249
227
 
250
- **Multi-language:**
251
- ```json
252
- {
253
- "welcome": {
254
- "key": "home.welcome",
255
- "defaultValue": {
256
- "ko": "환영합니다",
257
- "en": "Welcome",
258
- "ja": "ようこそ"
228
+ ### 🔧 Template Variables
229
+
230
+ ```typescript
231
+ defineLabels({
232
+ home: {
233
+ welcome: {
234
+ en: "Welcome {name}, you have {count} messages",
235
+ ko: "{name}님, {count}개의 메시지가 있습니다"
236
+ }
259
237
  }
260
- }
261
- }
262
- ```
238
+ });
263
239
 
264
- **Variable placeholders:**
265
- ```json
266
- {
267
- "greeting": {
268
- "key": "home.greeting",
269
- "defaultValue": "Hello, {name}!"
270
- }
271
- }
272
- ```
240
+ const label = await getLabel('home');
241
+ const text = label.welcome; // Direct access!
273
242
 
274
- Usage:
275
- ```typescript
276
- t('greeting', undefined, { name: 'John' })
277
- // → "Hello, John!"
243
+ format(text, { name: "John", count: 5 });
244
+ // Output: "John님, 5개의 메시지가 있습니다"
278
245
  ```
279
246
 
280
- ## Configuration
247
+ ### 🍪 Smart Locale Detection
281
248
 
282
- ### Environment Variables
249
+ Automatic locale detection with priority:
250
+ 1. User's cookie (`cms-locale`)
251
+ 2. Config's `defaultLocale`
252
+ 3. Final fallback: `'en'`
283
253
 
284
- Configure CMS behavior via environment variables in `.env.local`:
285
-
286
- ```bash
287
- # Default locale (default: 'en')
288
- SPFN_CMS_DEFAULT_LOCALE=ko
254
+ ```typescript
255
+ // User sets locale (saved to cookie)
256
+ await setLocale('ko');
289
257
 
290
- # Supported locales, comma-separated (default: 'en,ko')
291
- SPFN_CMS_SUPPORTED_LOCALES=en,ko,ja
258
+ // Automatically uses 'ko' locale
259
+ const label = await getLabel('home');
260
+ label.hero.title; // "환영합니다" (Korean)
292
261
 
293
- # Auto-detect browser language (default: true)
294
- SPFN_CMS_DETECT_BROWSER_LANGUAGE=true
262
+ // Switch to English
263
+ await setLocale('en');
264
+ const label2 = await getLabel('home');
265
+ label2.hero.title; // "Welcome" (English)
295
266
  ```
296
267
 
297
- ### Runtime Configuration
268
+ ### 🔄 Auto-Sync
298
269
 
299
- Override configuration at runtime (mainly for testing):
270
+ Labels synchronize automatically on server startup:
271
+ - ✅ Creates new labels
272
+ - ✅ Updates changed labels (deep equality check)
273
+ - ✅ Skips unchanged labels (performance)
274
+ - ✅ Rebuilds published cache
275
+ - ⚠️ Optionally removes orphaned labels
300
276
 
301
- ```typescript
302
- import { configureCms, getCmsConfig } from '@spfn/cms';
303
-
304
- // Get current configuration
305
- const config = getCmsConfig();
306
- console.log(config.defaultLocale); // 'ko'
307
- console.log(config.supportedLocales); // ['ko', 'en']
308
-
309
- // Update configuration
310
- configureCms({
311
- defaultLocale: 'en',
312
- supportedLocales: ['en', 'ko', 'ja'],
313
- detectBrowserLanguage: false
314
- });
315
- ```
316
-
317
- ## Locale Management
277
+ ## API Reference
318
278
 
319
- ### Automatic Locale Detection
279
+ ### createCmsClient()
320
280
 
321
- The CMS automatically manages user locale with the following priority:
281
+ Factory function to create CMS client with API, getLabel, getLabels, and format utilities.
322
282
 
323
- 1. **Cookie** - User's explicitly selected locale (persisted)
324
- 2. **Browser Language** - Auto-detected from `Accept-Language` header (if enabled)
325
- 3. **Default Locale** - System default from environment variables
283
+ ```typescript
284
+ const { api, getLabel, getLabels, format } = createCmsClient(labelsDefinition, labelConfig);
285
+ ```
326
286
 
327
- ### Server Actions (`@spfn/cms/actions`)
287
+ **Returns:**
288
+ - `api` - API client for CMS routes
289
+ - `getLabel(section)` - Fetch a single section (direct access)
290
+ - `getLabels(sections)` - Fetch multiple sections (with section names)
291
+ - `format(template, vars)` - Template variable substitution
328
292
 
329
- Use Server Actions for locale management in both server and client components:
293
+ ### getLabel()
330
294
 
331
- **Get current locale:**
295
+ Fetch a **single section** from published cache with direct access (no section name wrapper).
332
296
 
333
297
  ```typescript
334
- // Server Component
335
- import { getLocale } from '@spfn/cms/actions';
298
+ // Single section - direct access
299
+ const label = await getLabel('home');
300
+ label.hero.title; // Direct access without 'home.' prefix
301
+ label.cta;
302
+ ```
336
303
 
337
- export default async function RootLayout({ children }) {
338
- const locale = await getLocale();
304
+ **Use when:**
305
+ - You need labels from only ONE section
306
+ - You want cleaner, direct access to properties
307
+ - Most common use case for individual pages
339
308
 
340
- return <html lang={locale}>{children}</html>;
341
- }
342
- ```
309
+ **Returns:** Labels directly without section name wrapper
343
310
 
344
- ```typescript
345
- // Client Component
346
- 'use client';
347
- import { getLocale } from '@spfn/cms/actions';
348
- import { useEffect, useState } from 'react';
311
+ **Features:**
312
+ - Auto locale detection (cookie → defaultLocale → 'en')
313
+ - Direct property access (no section name)
314
+ - Type-safe with IDE autocomplete
315
+ - Merges published cache with defaults
349
316
 
350
- export default function LanguageSwitcher() {
351
- const [locale, setLocale] = useState('');
317
+ ### getLabels()
352
318
 
353
- useEffect(() => {
354
- getLocale().then(setLocale);
355
- }, []);
319
+ Fetch **multiple sections** from published cache with section names as keys.
356
320
 
357
- return <div>Current: {locale}</div>;
358
- }
321
+ ```typescript
322
+ // Multiple sections - with section names
323
+ const labels = await getLabels(['home', 'about']);
324
+ labels.home.hero.title; // Access via section name
325
+ labels.about.title;
359
326
  ```
360
327
 
361
- **Change locale:**
328
+ **Use when:**
329
+ - You need labels from MULTIPLE sections
330
+ - You're building a page that uses multiple label groups
362
331
 
363
- ```typescript
364
- import { setLocale } from '@spfn/cms/actions';
332
+ **Returns:** Object with section names as keys
365
333
 
366
- async function changeLanguage(newLocale: string) {
367
- await setLocale(newLocale);
368
- window.location.reload(); // Reload to apply changes
369
- }
370
- ```
334
+ **Features:**
335
+ - Auto locale detection (cookie → defaultLocale → 'en')
336
+ - Section filtering (only processes requested sections)
337
+ - Type-safe: only requested sections available
338
+ - Merges published cache with defaults
371
339
 
372
- **Get supported locales:**
340
+ **Performance:**
341
+ - Only requested sections are processed (not entire labelsDefinition)
342
+ - 10x faster when requesting specific sections
343
+ - Reduces CPU and memory usage proportionally
373
344
 
374
- ```typescript
375
- import { getLocales } from '@spfn/cms/actions';
345
+ ### format()
346
+
347
+ Replace template variables in strings.
376
348
 
377
- const locales = await getLocales(); // ['ko', 'en', 'ja']
349
+ ```typescript
350
+ format("Hello {name}!", { name: "John" }); // "Hello John!"
351
+ format("{count} items", { count: 5 }); // "5 items"
378
352
  ```
379
353
 
380
- ### Auto-detect Locale in Server Components
354
+ **Syntax:** `{variableName}` - Supports strings and numbers
381
355
 
382
- When `locale` is not specified, `getSection()` automatically uses the detected locale:
356
+ ### setLocale() / getLocale()
383
357
 
384
- ```typescript
385
- import { getSection } from '@spfn/cms/server';
358
+ Server actions for locale management (cookie-based).
386
359
 
387
- // Auto-detects locale from cookie → browser → default
388
- const { t } = await getSection('home');
360
+ ```typescript
361
+ // Set user's preferred locale
362
+ await setLocale('ko'); // Saves to 'cms-locale' cookie
389
363
 
390
- // Or explicitly specify locale
391
- const { t: tEn } = await getSection('home', 'en');
364
+ // Get current locale
365
+ const locale = await getLocale(defaultLocale); // Returns: cookie defaultLocale 'en'
392
366
  ```
393
367
 
394
- ## Documentation
368
+ **Cookie settings:**
369
+ - Name: `cms-locale`
370
+ - Max age: 1 year
371
+ - HttpOnly, Secure (production), SameSite: lax
395
372
 
396
- - **[Label Auto-Sync Guide](./LABEL_SYNC_GUIDE.md)** - Detailed configuration guide
397
- - **[Examples](./examples/)** - Usage examples
373
+ ### syncLabels()
398
374
 
399
- ## Architecture
375
+ Synchronize labels to database (server-side only).
400
376
 
377
+ ```typescript
378
+ await syncLabels(labelsDefinition, {
379
+ removeOrphaned: false, // Delete labels not in code
380
+ dryRun: false // Preview changes without applying
381
+ });
401
382
  ```
402
- JSON Files (src/cms/labels/**/*.json)
403
-
404
- loadLabelsFromJson()
405
-
406
- ┌─────────────────────┐
407
- │ LabelSyncGenerator │ ← File watcher (development)
408
- │ initLabelSync() │ ← Server startup
409
- └─────────────────────┘
410
-
411
- syncAll()
412
-
413
- ┌─────────────────────┐
414
- │ PostgreSQL DB │
415
- │ - cms_labels │
416
- │ - published_cache │ ⭐ Used by API
417
- └─────────────────────┘
418
- ↓ HTTP API
419
- ┌─────────────────────┐
420
- │ Application │
421
- │ - getSection() │
422
- │ - useSection() │
423
- └─────────────────────┘
424
- ```
425
-
426
- ## API Reference
427
383
 
428
- ### Server-side API
384
+ **Returns:** `{ added, updated, removed, unchanged }`
429
385
 
430
- - `getSection(section, locale?)` - Get section labels (auto-detects locale if not specified)
431
- - `getSections(sections, locale?)` - Get multiple sections (auto-detects locale if not specified)
432
- - `initLabelSync(options?)` - Sync labels on server startup
433
-
434
- ### Server Actions API (`@spfn/cms/actions`)
386
+ ## Architecture
435
387
 
436
- Available for both server and client components:
388
+ ### Database Schema
437
389
 
438
- - `getLocale()` - Get current locale (cookie → browser → default)
439
- - `setLocale(locale)` - Set locale (saves to cookie)
440
- - `getLocales()` - Get supported locale list
441
- - `LOCALE_COOKIE_KEY` - Locale cookie key constant
390
+ ```
391
+ cms_labels (metadata)
392
+ ├─ id, key, section, type, defaultValue
393
+ └─ publishedVersion
442
394
 
443
- ### Configuration API
395
+ cms_label_values (actual content)
396
+ ├─ labelId, version, locale, breakpoint
397
+ └─ value (JSONB)
444
398
 
445
- - `getCmsConfig()` - Get current CMS configuration
446
- - `configureCms(config)` - Update configuration (runtime)
447
- - `resetCmsConfig()` - Reset configuration to defaults
399
+ cms_published_cache (performance)
400
+ ├─ section, locale, content (JSONB)
401
+ └─ version (for cache invalidation)
448
402
 
449
- ### Client-side API (`@spfn/cms/client`)
403
+ cms_audit_logs (tracking)
404
+ └─ action, userId, changes, metadata
405
+ ```
450
406
 
451
- - `useSection(section, options?)` - Section labels hook
452
- - `useSections(sections)` - Multiple sections hook
453
- - `useCmsStore()` - CMS store hook
454
- - `cmsApi` - CMS API client
455
- - `InitCms` - Client initialization component
407
+ ### Query Flow
456
408
 
457
- ### Sync API
409
+ 1. **getLabels()** → published_cache (single query, 5ms)
410
+ 2. **Fallback** → bindLocale(defaults)
411
+ 3. **Merge** → cache overrides defaults
412
+ 4. **Return** → type-safe nested object
458
413
 
459
- - `loadLabelsFromJson(labelsDir)` - Load labels from JSON files
460
- - `syncAll(sections, options?)` - Sync all sections
461
- - `syncSection(definition, options?)` - Sync specific section
414
+ ## Performance
462
415
 
463
- ### Codegen Integration
416
+ - **Published cache:** 5ms (vs 87ms with JOINs) - 17x faster
417
+ - **N+1 prevention:** Bulk section queries with `inArray()`
418
+ - **Section filtering:** Only requested sections processed (10x faster for selective access)
419
+ - **Unchanged labels:** Skipped during sync (deep equality check)
420
+ - **Client caching:** Version-based invalidation
464
421
 
465
- - `createLabelSyncGenerator(config?)` - Generator factory
466
- - `LabelSyncGenerator` - Generator class
422
+ ### Example: Large Scale
467
423
 
468
- ## Development Workflow
424
+ ```typescript
425
+ // 10 sections with 100 labels each = 1,000 total labels
426
+ const labelsDefinition = {
427
+ home: { /* 100 labels */ },
428
+ about: { /* 100 labels */ },
429
+ products: { /* 100 labels */ },
430
+ // ... 7 more sections
431
+ };
469
432
 
470
- 1. **Create/Edit JSON files** in `src/cms/labels/`
471
- 2. **Auto-sync happens** (if dev server is running)
472
- 3. **Labels immediately available** via `getSection()` or `useSection()`
433
+ // Only request 'home' - direct access
434
+ const label = await getLabel('home');
473
435
 
474
- **Example:**
436
+ // ✅ Performance: Processes only 100 labels (10% of total)
437
+ // ❌ Without filtering: Would process all 1,000 labels
438
+ ```
475
439
 
476
- ```bash
477
- # Terminal 1: Start dev server
478
- pnpm dev
440
+ **Benefits:**
441
+ - CPU usage: 10x reduction (100 vs 1,000 labels)
442
+ - Memory usage: 10x reduction
443
+ - Response time: Proportionally faster
479
444
 
480
- # Terminal 2: Edit label file
481
- echo '{"test": {"key": "layout.test", "defaultValue": "Test"}}' > src/cms/labels/layout/test.json
445
+ ## Development Status
482
446
 
483
- # Auto-sync triggers
484
- # ✅ Label sync completed
485
- # Created: 1
486
- ```
447
+ This package is currently in alpha. APIs may change.
487
448
 
488
449
  ## License
489
450
 
490
- MIT
451
+ MIT