@spfn/cms 0.1.0-alpha.88 → 0.2.0-beta.1
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/README.md +399 -50
- package/dist/actions.d.ts +15 -2
- package/dist/actions.js +15 -89
- package/dist/actions.js.map +1 -1
- package/dist/config.d.ts +39 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +149 -0
- package/dist/errors.js +164 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +107 -85
- package/dist/index.js +197 -610
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +41 -148
- package/dist/server.js +473 -1619
- package/dist/server.js.map +1 -1
- package/migrations/0000_medical_ozymandias.sql +44 -0
- package/migrations/meta/0000_snapshot.json +8 -237
- package/migrations/meta/_journal.json +2 -2
- package/package.json +26 -35
- package/dist/actions-BEFWwQsh.d.ts +0 -195
- package/dist/api.d.ts +0 -319
- package/dist/api.js +0 -467
- package/dist/api.js.map +0 -1
- package/dist/client.d.ts +0 -146
- package/dist/client.js +0 -1321
- package/dist/client.js.map +0 -1
- package/dist/index-Dh5FjWzR.d.ts +0 -112
- package/dist/label-sync-generator-B0EmvtWM.d.ts +0 -32
- package/dist/lib/contracts/labels.d.ts +0 -244
- package/dist/lib/contracts/labels.js +0 -269
- package/dist/lib/contracts/labels.js.map +0 -1
- package/dist/lib/contracts/published-cache.d.ts +0 -48
- package/dist/lib/contracts/published-cache.js +0 -49
- package/dist/lib/contracts/published-cache.js.map +0 -1
- package/dist/lib/contracts/values.d.ts +0 -71
- package/dist/lib/contracts/values.js +0 -104
- package/dist/lib/contracts/values.js.map +0 -1
- package/dist/locale.constants-BNkSdNP1.d.ts +0 -108
- package/dist/server/entities/cms-audit-logs.d.ts +0 -158
- package/dist/server/entities/cms-audit-logs.js +0 -78
- package/dist/server/entities/cms-audit-logs.js.map +0 -1
- package/dist/server/entities/cms-draft-cache.d.ts +0 -128
- package/dist/server/entities/cms-draft-cache.js +0 -42
- package/dist/server/entities/cms-draft-cache.js.map +0 -1
- package/dist/server/entities/cms-label-values.d.ts +0 -141
- package/dist/server/entities/cms-label-values.js +0 -81
- package/dist/server/entities/cms-label-values.js.map +0 -1
- package/dist/server/entities/cms-labels.d.ts +0 -192
- package/dist/server/entities/cms-labels.js +0 -46
- package/dist/server/entities/cms-labels.js.map +0 -1
- package/dist/server/entities/cms-published-cache.d.ts +0 -144
- package/dist/server/entities/cms-published-cache.js +0 -40
- package/dist/server/entities/cms-published-cache.js.map +0 -1
- package/dist/server/entities/cms-schema.d.ts +0 -5
- package/dist/server/entities/cms-schema.js +0 -7
- package/dist/server/entities/cms-schema.js.map +0 -1
- package/dist/server/entities/index.d.ts +0 -6
- package/dist/server/entities/index.js +0 -181
- package/dist/server/entities/index.js.map +0 -1
- package/dist/server/generators/index.d.ts +0 -19
- package/dist/server/generators/index.js +0 -727
- package/dist/server/generators/index.js.map +0 -1
- package/dist/server/labels/index.d.ts +0 -1
- package/dist/server/labels/index.js +0 -33
- package/dist/server/labels/index.js.map +0 -1
- package/dist/server/repositories/index.d.ts +0 -212
- package/dist/server/repositories/index.js +0 -414
- package/dist/server/repositories/index.js.map +0 -1
- package/dist/server/routes/labels/[id]/admin/index.js +0 -675
- package/dist/server/routes/labels/[id]/admin/index.js.map +0 -1
- package/dist/server/routes/labels/[id]/index.js +0 -572
- package/dist/server/routes/labels/[id]/index.js.map +0 -1
- package/dist/server/routes/labels/[id]/publish/index.js +0 -716
- package/dist/server/routes/labels/[id]/publish/index.js.map +0 -1
- package/dist/server/routes/labels/[id]/versions/index.js +0 -544
- package/dist/server/routes/labels/[id]/versions/index.js.map +0 -1
- package/dist/server/routes/labels/_id_/admin/index.d.ts +0 -11
- package/dist/server/routes/labels/_id_/index.d.ts +0 -12
- package/dist/server/routes/labels/_id_/publish/index.d.ts +0 -11
- package/dist/server/routes/labels/_id_/versions/index.d.ts +0 -11
- package/dist/server/routes/labels/by-key/[key]/index.js +0 -521
- package/dist/server/routes/labels/by-key/[key]/index.js.map +0 -1
- package/dist/server/routes/labels/by-key/_key_/index.d.ts +0 -10
- package/dist/server/routes/labels/index.d.ts +0 -12
- package/dist/server/routes/labels/index.js +0 -680
- package/dist/server/routes/labels/index.js.map +0 -1
- package/dist/server/routes/published-cache/index.d.ts +0 -11
- package/dist/server/routes/published-cache/index.js +0 -333
- package/dist/server/routes/published-cache/index.js.map +0 -1
- package/dist/server/routes/values/[labelId]/[version]/index.js +0 -453
- package/dist/server/routes/values/[labelId]/[version]/index.js.map +0 -1
- package/dist/server/routes/values/[labelId]/index.js +0 -448
- package/dist/server/routes/values/[labelId]/index.js.map +0 -1
- package/dist/server/routes/values/_labelId_/_version_/index.d.ts +0 -10
- package/dist/server/routes/values/_labelId_/index.d.ts +0 -10
- package/migrations/0000_milky_blockbuster.sql +0 -72
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# @spfn/cms
|
|
2
2
|
|
|
3
|
-
Content Management System for Next.js with
|
|
3
|
+
Type-safe Content Management System for Next.js with automatic database synchronization and published cache.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- 🔄 Auto-sync
|
|
9
|
-
- 🌐
|
|
10
|
-
- 🍪 Cookie-based
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -20,83 +21,431 @@ pnpm spfn add @spfn/cms
|
|
|
20
21
|
|
|
21
22
|
## Quick Start
|
|
22
23
|
|
|
23
|
-
### 1.
|
|
24
|
+
### 1. Define Labels & Configuration
|
|
24
25
|
|
|
25
|
-
```
|
|
26
|
-
//
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
});
|
|
36
|
+
|
|
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: "회사 소개" }
|
|
33
49
|
}
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Create client with API, getLabel, getLabels, and format
|
|
53
|
+
export const { api, getLabel, getLabels, format } = createCmsClient(
|
|
54
|
+
labelsDefinition,
|
|
55
|
+
labelConfig
|
|
56
|
+
);
|
|
36
57
|
```
|
|
37
58
|
|
|
38
59
|
### 2. Enable Auto-Sync
|
|
39
60
|
|
|
40
61
|
```typescript
|
|
41
|
-
//
|
|
42
|
-
import {
|
|
62
|
+
// server.config.ts
|
|
63
|
+
import { defineServerConfig } from '@spfn/core/server';
|
|
64
|
+
import { syncLabels } from '@spfn/cms/server';
|
|
65
|
+
import { labelsDefinition } from './labels';
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Option 1: Single definition
|
|
69
|
+
export default defineServerConfig()
|
|
70
|
+
.lifecycle({
|
|
71
|
+
afterInfrastructure: async () => {
|
|
72
|
+
await syncLabels(labelsDefinition);
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.build();
|
|
76
|
+
|
|
77
|
+
// Option 2: Multiple definitions (organized in separate files)
|
|
78
|
+
import { homeLabels } from './labels/home';
|
|
79
|
+
import { aboutLabels } from './labels/about';
|
|
43
80
|
|
|
44
|
-
export default
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
81
|
+
export default defineServerConfig()
|
|
82
|
+
.lifecycle({
|
|
83
|
+
afterInfrastructure: async () => {
|
|
84
|
+
await syncLabels([homeLabels, aboutLabels]);
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.build();
|
|
49
88
|
```
|
|
50
89
|
|
|
51
90
|
### 3. Use in Your App
|
|
52
91
|
|
|
53
|
-
**Server Component:**
|
|
92
|
+
**Server Component (Single Section):**
|
|
54
93
|
|
|
55
94
|
```typescript
|
|
56
|
-
import {
|
|
95
|
+
import { getLabel, format } from '@/labels';
|
|
57
96
|
|
|
58
97
|
export default async function HomePage() {
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
);
|
|
61
112
|
}
|
|
62
113
|
```
|
|
63
114
|
|
|
64
|
-
**
|
|
115
|
+
**Server Component (Multiple Sections):**
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
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
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Locale Management:**
|
|
65
134
|
|
|
66
135
|
```typescript
|
|
67
136
|
'use client';
|
|
68
|
-
import {
|
|
137
|
+
import { setLocale } from '@spfn/cms/actions';
|
|
69
138
|
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
139
|
+
export function LanguageSwitcher() {
|
|
140
|
+
return (
|
|
141
|
+
<div>
|
|
142
|
+
<button onClick={() => setLocale('ko')}>한국어</button>
|
|
143
|
+
<button onClick={() => setLocale('en')}>English</button>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
73
146
|
}
|
|
74
147
|
```
|
|
75
148
|
|
|
76
|
-
##
|
|
149
|
+
## Key Features
|
|
77
150
|
|
|
78
|
-
|
|
151
|
+
### 🎯 Type Safety
|
|
79
152
|
|
|
80
|
-
|
|
81
|
-
- [Label Sync Guide](../../docs/ecosystem/cms/label-sync.md) - Auto-sync options
|
|
82
|
-
- [Advanced Features](../../docs/ecosystem/cms/advanced-features.md) - Breakpoints, value types, Draft Mode
|
|
83
|
-
- [Locale Management](../../docs/ecosystem/cms/locale-management.md) - 50+ languages guide
|
|
84
|
-
- [API Reference](../../docs/ecosystem/cms/api-reference.md) - Complete API docs
|
|
85
|
-
- [Draft & Versioning](../../docs/ecosystem/cms/draft-versioning.md) - Version control & audit logs
|
|
153
|
+
**Section Name Validation:**
|
|
86
154
|
|
|
87
|
-
|
|
155
|
+
```typescript
|
|
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
|
|
88
159
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
164
|
+
|
|
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
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Property Access Validation:**
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Single section - direct access
|
|
176
|
+
const label = await getLabel('signup');
|
|
177
|
+
|
|
178
|
+
// ✅ IDE autocomplete works perfectly
|
|
179
|
+
label.title; // OK - direct access
|
|
180
|
+
label.userName; // OK
|
|
181
|
+
label.email; // OK
|
|
182
|
+
|
|
183
|
+
// ❌ Typos are caught at compile time
|
|
184
|
+
label.titlee; // ❌ Compile error
|
|
185
|
+
label.userName; // ❌ Compile error (typo)
|
|
186
|
+
|
|
187
|
+
// Multiple sections - with section names
|
|
188
|
+
const labels = await getLabels(['home', 'about']);
|
|
189
|
+
labels.home.hero.title; // OK
|
|
190
|
+
labels.about.title; // OK
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**API Distinction:**
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// getLabel: Single section, direct access
|
|
197
|
+
const signup = await getLabel('signup');
|
|
198
|
+
signup.title; // ✅ Direct access (cleaner!)
|
|
199
|
+
|
|
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
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 🎨 Nested Structure
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
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
|
+
});
|
|
220
|
+
|
|
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; // "보안"
|
|
226
|
+
```
|
|
227
|
+
|
|
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
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const label = await getLabel('home');
|
|
241
|
+
const text = label.welcome; // Direct access!
|
|
242
|
+
|
|
243
|
+
format(text, { name: "John", count: 5 });
|
|
244
|
+
// Output: "John님, 5개의 메시지가 있습니다"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 🍪 Smart Locale Detection
|
|
248
|
+
|
|
249
|
+
Automatic locale detection with priority:
|
|
250
|
+
1. User's cookie (`cms-locale`)
|
|
251
|
+
2. Config's `defaultLocale`
|
|
252
|
+
3. Final fallback: `'en'`
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// User sets locale (saved to cookie)
|
|
256
|
+
await setLocale('ko');
|
|
257
|
+
|
|
258
|
+
// Automatically uses 'ko' locale
|
|
259
|
+
const label = await getLabel('home');
|
|
260
|
+
label.hero.title; // "환영합니다" (Korean)
|
|
261
|
+
|
|
262
|
+
// Switch to English
|
|
263
|
+
await setLocale('en');
|
|
264
|
+
const label2 = await getLabel('home');
|
|
265
|
+
label2.hero.title; // "Welcome" (English)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 🔄 Auto-Sync
|
|
269
|
+
|
|
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
|
|
276
|
+
|
|
277
|
+
## API Reference
|
|
278
|
+
|
|
279
|
+
### createCmsClient()
|
|
280
|
+
|
|
281
|
+
Factory function to create CMS client with API, getLabel, getLabels, and format utilities.
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
const { api, getLabel, getLabels, format } = createCmsClient(labelsDefinition, labelConfig);
|
|
285
|
+
```
|
|
286
|
+
|
|
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
|
|
292
|
+
|
|
293
|
+
### getLabel()
|
|
294
|
+
|
|
295
|
+
Fetch a **single section** from published cache with direct access (no section name wrapper).
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// Single section - direct access
|
|
299
|
+
const label = await getLabel('home');
|
|
300
|
+
label.hero.title; // Direct access without 'home.' prefix
|
|
301
|
+
label.cta;
|
|
302
|
+
```
|
|
303
|
+
|
|
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
|
|
308
|
+
|
|
309
|
+
**Returns:** Labels directly without section name wrapper
|
|
310
|
+
|
|
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
|
|
316
|
+
|
|
317
|
+
### getLabels()
|
|
318
|
+
|
|
319
|
+
Fetch **multiple sections** from published cache with section names as keys.
|
|
320
|
+
|
|
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;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Use when:**
|
|
329
|
+
- You need labels from MULTIPLE sections
|
|
330
|
+
- You're building a page that uses multiple label groups
|
|
331
|
+
|
|
332
|
+
**Returns:** Object with section names as keys
|
|
333
|
+
|
|
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
|
|
339
|
+
|
|
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
|
|
344
|
+
|
|
345
|
+
### format()
|
|
346
|
+
|
|
347
|
+
Replace template variables in strings.
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
format("Hello {name}!", { name: "John" }); // "Hello John!"
|
|
351
|
+
format("{count} items", { count: 5 }); // "5 items"
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Syntax:** `{variableName}` - Supports strings and numbers
|
|
355
|
+
|
|
356
|
+
### setLocale() / getLocale()
|
|
357
|
+
|
|
358
|
+
Server actions for locale management (cookie-based).
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// Set user's preferred locale
|
|
362
|
+
await setLocale('ko'); // Saves to 'cms-locale' cookie
|
|
363
|
+
|
|
364
|
+
// Get current locale
|
|
365
|
+
const locale = await getLocale(defaultLocale); // Returns: cookie → defaultLocale → 'en'
|
|
94
366
|
```
|
|
95
367
|
|
|
368
|
+
**Cookie settings:**
|
|
369
|
+
- Name: `cms-locale`
|
|
370
|
+
- Max age: 1 year
|
|
371
|
+
- HttpOnly, Secure (production), SameSite: lax
|
|
372
|
+
|
|
373
|
+
### syncLabels()
|
|
374
|
+
|
|
375
|
+
Synchronize labels to database (server-side only).
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
await syncLabels(labelsDefinition, {
|
|
379
|
+
removeOrphaned: false, // Delete labels not in code
|
|
380
|
+
dryRun: false // Preview changes without applying
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Returns:** `{ added, updated, removed, unchanged }`
|
|
385
|
+
|
|
386
|
+
## Architecture
|
|
387
|
+
|
|
388
|
+
### Database Schema
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
cms_labels (metadata)
|
|
392
|
+
├─ id, key, section, type, defaultValue
|
|
393
|
+
└─ publishedVersion
|
|
394
|
+
|
|
395
|
+
cms_label_values (actual content)
|
|
396
|
+
├─ labelId, version, locale, breakpoint
|
|
397
|
+
└─ value (JSONB)
|
|
398
|
+
|
|
399
|
+
cms_published_cache (performance)
|
|
400
|
+
├─ section, locale, content (JSONB)
|
|
401
|
+
└─ version (for cache invalidation)
|
|
402
|
+
|
|
403
|
+
cms_audit_logs (tracking)
|
|
404
|
+
└─ action, userId, changes, metadata
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Query Flow
|
|
408
|
+
|
|
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
|
|
413
|
+
|
|
414
|
+
## Performance
|
|
415
|
+
|
|
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
|
|
421
|
+
|
|
422
|
+
### Example: Large Scale
|
|
423
|
+
|
|
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
|
+
};
|
|
432
|
+
|
|
433
|
+
// Only request 'home' - direct access
|
|
434
|
+
const label = await getLabel('home');
|
|
435
|
+
|
|
436
|
+
// ✅ Performance: Processes only 100 labels (10% of total)
|
|
437
|
+
// ❌ Without filtering: Would process all 1,000 labels
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Benefits:**
|
|
441
|
+
- CPU usage: 10x reduction (100 vs 1,000 labels)
|
|
442
|
+
- Memory usage: 10x reduction
|
|
443
|
+
- Response time: Proportionally faster
|
|
444
|
+
|
|
96
445
|
## Development Status
|
|
97
446
|
|
|
98
447
|
This package is currently in alpha. APIs may change.
|
|
99
448
|
|
|
100
449
|
## License
|
|
101
450
|
|
|
102
|
-
MIT
|
|
451
|
+
MIT
|
package/dist/actions.d.ts
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Set user's preferred locale in cookie
|
|
3
|
+
*
|
|
4
|
+
* @param locale - Language code (e.g., 'ko', 'en', 'ja')
|
|
5
|
+
*/
|
|
6
|
+
declare function setLocale(locale: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Get user's preferred locale from cookie
|
|
9
|
+
*
|
|
10
|
+
* @param defaultLocale - Default locale from labelConfig.defaultLocale
|
|
11
|
+
* @returns Language code (from cookie, or defaultLocale, or 'en')
|
|
12
|
+
*/
|
|
13
|
+
declare function getLocale(defaultLocale?: string): Promise<string>;
|
|
14
|
+
|
|
15
|
+
export { getLocale, setLocale };
|
package/dist/actions.js
CHANGED
|
@@ -1,100 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
import { cookies, headers } from "next/headers.js";
|
|
1
|
+
"use server";
|
|
3
2
|
|
|
4
|
-
// src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function getEnvBoolean(key, defaultValue) {
|
|
9
|
-
const value = process.env[key];
|
|
10
|
-
if (value === void 0) return defaultValue;
|
|
11
|
-
return value === "true" || value === "1";
|
|
12
|
-
}
|
|
13
|
-
function loadConfigFromEnv() {
|
|
14
|
-
const defaultLocale = getEnvVar("SPFN_CMS_DEFAULT_LOCALE", "en");
|
|
15
|
-
const supportedLocalesStr = getEnvVar("SPFN_CMS_SUPPORTED_LOCALES", "en,ko");
|
|
16
|
-
const detectBrowserLanguage2 = getEnvBoolean("SPFN_CMS_DETECT_BROWSER_LANGUAGE", true);
|
|
17
|
-
const locales = supportedLocalesStr.split(",").map((locale) => locale.trim()).filter((locale) => locale.length > 0);
|
|
18
|
-
if (!locales.includes(defaultLocale)) {
|
|
19
|
-
locales.unshift(defaultLocale);
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
defaultLocale,
|
|
23
|
-
locales,
|
|
24
|
-
supportedLocales: locales,
|
|
25
|
-
// backward compatibility
|
|
26
|
-
detectBrowserLanguage: detectBrowserLanguage2
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
var currentConfig = loadConfigFromEnv();
|
|
30
|
-
function getCmsConfig() {
|
|
31
|
-
return currentConfig;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// src/lib/constants/locale.constants.ts
|
|
35
|
-
var LOCALE_COOKIE_KEY = "spfn-locale";
|
|
36
|
-
|
|
37
|
-
// src/server/helpers/locale.actions.ts
|
|
38
|
-
async function detectBrowserLanguage() {
|
|
39
|
-
try {
|
|
40
|
-
const headersList = await headers();
|
|
41
|
-
const acceptLanguage = headersList.get("accept-language");
|
|
42
|
-
if (!acceptLanguage) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
const languages = acceptLanguage.split(",").map((lang) => {
|
|
46
|
-
const [code] = lang.split(";");
|
|
47
|
-
return code.split("-")[0].trim();
|
|
48
|
-
});
|
|
49
|
-
const config = getCmsConfig();
|
|
50
|
-
for (const lang of languages) {
|
|
51
|
-
if (config.locales.includes(lang)) {
|
|
52
|
-
return lang;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
async function getLocale() {
|
|
61
|
-
const config = getCmsConfig();
|
|
62
|
-
const cookieStore = await cookies();
|
|
63
|
-
const cookieLocale = cookieStore.get(LOCALE_COOKIE_KEY)?.value;
|
|
64
|
-
if (cookieLocale && config.locales.includes(cookieLocale)) {
|
|
65
|
-
return cookieLocale;
|
|
66
|
-
}
|
|
67
|
-
if (config.detectBrowserLanguage) {
|
|
68
|
-
const browserLang = await detectBrowserLanguage();
|
|
69
|
-
if (browserLang) {
|
|
70
|
-
return browserLang;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return config.defaultLocale;
|
|
74
|
-
}
|
|
3
|
+
// src/actions.ts
|
|
4
|
+
import { cookies } from "next/headers";
|
|
5
|
+
var LOCALE_COOKIE_NAME = "cms-locale";
|
|
6
|
+
var LOCALE_MAX_AGE = 365 * 24 * 60 * 60;
|
|
75
7
|
async function setLocale(locale) {
|
|
76
|
-
const config = getCmsConfig();
|
|
77
|
-
if (!config.locales.includes(locale)) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`Unsupported locale: ${locale}. Supported locales: ${config.locales.join(", ")}`
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
8
|
const cookieStore = await cookies();
|
|
83
|
-
cookieStore.set(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
9
|
+
cookieStore.set(LOCALE_COOKIE_NAME, locale, {
|
|
10
|
+
httpOnly: true,
|
|
11
|
+
secure: process.env.NODE_ENV === "production",
|
|
12
|
+
sameSite: "lax",
|
|
13
|
+
maxAge: LOCALE_MAX_AGE,
|
|
14
|
+
path: "/"
|
|
88
15
|
});
|
|
89
16
|
}
|
|
90
|
-
async function
|
|
91
|
-
const
|
|
92
|
-
|
|
17
|
+
async function getLocale(defaultLocale) {
|
|
18
|
+
const cookieStore = await cookies();
|
|
19
|
+
const localeCookie = cookieStore.get(LOCALE_COOKIE_NAME);
|
|
20
|
+
return localeCookie?.value ?? defaultLocale ?? "en";
|
|
93
21
|
}
|
|
94
22
|
export {
|
|
95
|
-
LOCALE_COOKIE_KEY,
|
|
96
23
|
getLocale,
|
|
97
|
-
getLocales,
|
|
98
24
|
setLocale
|
|
99
25
|
};
|
|
100
26
|
//# sourceMappingURL=actions.js.map
|