@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.
Files changed (97) hide show
  1. package/README.md +399 -50
  2. package/dist/actions.d.ts +15 -2
  3. package/dist/actions.js +15 -89
  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 +107 -85
  12. package/dist/index.js +197 -610
  13. package/dist/index.js.map +1 -1
  14. package/dist/server.d.ts +41 -148
  15. package/dist/server.js +473 -1619
  16. package/dist/server.js.map +1 -1
  17. package/migrations/0000_medical_ozymandias.sql +44 -0
  18. package/migrations/meta/0000_snapshot.json +8 -237
  19. package/migrations/meta/_journal.json +2 -2
  20. package/package.json +26 -35
  21. package/dist/actions-BEFWwQsh.d.ts +0 -195
  22. package/dist/api.d.ts +0 -319
  23. package/dist/api.js +0 -467
  24. package/dist/api.js.map +0 -1
  25. package/dist/client.d.ts +0 -146
  26. package/dist/client.js +0 -1321
  27. package/dist/client.js.map +0 -1
  28. package/dist/index-Dh5FjWzR.d.ts +0 -112
  29. package/dist/label-sync-generator-B0EmvtWM.d.ts +0 -32
  30. package/dist/lib/contracts/labels.d.ts +0 -244
  31. package/dist/lib/contracts/labels.js +0 -269
  32. package/dist/lib/contracts/labels.js.map +0 -1
  33. package/dist/lib/contracts/published-cache.d.ts +0 -48
  34. package/dist/lib/contracts/published-cache.js +0 -49
  35. package/dist/lib/contracts/published-cache.js.map +0 -1
  36. package/dist/lib/contracts/values.d.ts +0 -71
  37. package/dist/lib/contracts/values.js +0 -104
  38. package/dist/lib/contracts/values.js.map +0 -1
  39. package/dist/locale.constants-BNkSdNP1.d.ts +0 -108
  40. package/dist/server/entities/cms-audit-logs.d.ts +0 -158
  41. package/dist/server/entities/cms-audit-logs.js +0 -78
  42. package/dist/server/entities/cms-audit-logs.js.map +0 -1
  43. package/dist/server/entities/cms-draft-cache.d.ts +0 -128
  44. package/dist/server/entities/cms-draft-cache.js +0 -42
  45. package/dist/server/entities/cms-draft-cache.js.map +0 -1
  46. package/dist/server/entities/cms-label-values.d.ts +0 -141
  47. package/dist/server/entities/cms-label-values.js +0 -81
  48. package/dist/server/entities/cms-label-values.js.map +0 -1
  49. package/dist/server/entities/cms-labels.d.ts +0 -192
  50. package/dist/server/entities/cms-labels.js +0 -46
  51. package/dist/server/entities/cms-labels.js.map +0 -1
  52. package/dist/server/entities/cms-published-cache.d.ts +0 -144
  53. package/dist/server/entities/cms-published-cache.js +0 -40
  54. package/dist/server/entities/cms-published-cache.js.map +0 -1
  55. package/dist/server/entities/cms-schema.d.ts +0 -5
  56. package/dist/server/entities/cms-schema.js +0 -7
  57. package/dist/server/entities/cms-schema.js.map +0 -1
  58. package/dist/server/entities/index.d.ts +0 -6
  59. package/dist/server/entities/index.js +0 -181
  60. package/dist/server/entities/index.js.map +0 -1
  61. package/dist/server/generators/index.d.ts +0 -19
  62. package/dist/server/generators/index.js +0 -727
  63. package/dist/server/generators/index.js.map +0 -1
  64. package/dist/server/labels/index.d.ts +0 -1
  65. package/dist/server/labels/index.js +0 -33
  66. package/dist/server/labels/index.js.map +0 -1
  67. package/dist/server/repositories/index.d.ts +0 -212
  68. package/dist/server/repositories/index.js +0 -414
  69. package/dist/server/repositories/index.js.map +0 -1
  70. package/dist/server/routes/labels/[id]/admin/index.js +0 -675
  71. package/dist/server/routes/labels/[id]/admin/index.js.map +0 -1
  72. package/dist/server/routes/labels/[id]/index.js +0 -572
  73. package/dist/server/routes/labels/[id]/index.js.map +0 -1
  74. package/dist/server/routes/labels/[id]/publish/index.js +0 -716
  75. package/dist/server/routes/labels/[id]/publish/index.js.map +0 -1
  76. package/dist/server/routes/labels/[id]/versions/index.js +0 -544
  77. package/dist/server/routes/labels/[id]/versions/index.js.map +0 -1
  78. package/dist/server/routes/labels/_id_/admin/index.d.ts +0 -11
  79. package/dist/server/routes/labels/_id_/index.d.ts +0 -12
  80. package/dist/server/routes/labels/_id_/publish/index.d.ts +0 -11
  81. package/dist/server/routes/labels/_id_/versions/index.d.ts +0 -11
  82. package/dist/server/routes/labels/by-key/[key]/index.js +0 -521
  83. package/dist/server/routes/labels/by-key/[key]/index.js.map +0 -1
  84. package/dist/server/routes/labels/by-key/_key_/index.d.ts +0 -10
  85. package/dist/server/routes/labels/index.d.ts +0 -12
  86. package/dist/server/routes/labels/index.js +0 -680
  87. package/dist/server/routes/labels/index.js.map +0 -1
  88. package/dist/server/routes/published-cache/index.d.ts +0 -11
  89. package/dist/server/routes/published-cache/index.js +0 -333
  90. package/dist/server/routes/published-cache/index.js.map +0 -1
  91. package/dist/server/routes/values/[labelId]/[version]/index.js +0 -453
  92. package/dist/server/routes/values/[labelId]/[version]/index.js.map +0 -1
  93. package/dist/server/routes/values/[labelId]/index.js +0 -448
  94. package/dist/server/routes/values/[labelId]/index.js.map +0 -1
  95. package/dist/server/routes/values/_labelId_/_version_/index.d.ts +0 -10
  96. package/dist/server/routes/values/_labelId_/index.d.ts +0 -10
  97. 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 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
8
- - 🔄 Auto-sync to database
9
- - 🌐 50+ languages support
10
- - 🍪 Cookie-based locale management
11
- - 🔥 Hot reload during development
12
- - 💾 Published cache (17x faster)
13
- - 📝 Draft system & version control
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. Create Label Files
24
+ ### 1. Define Labels & Configuration
24
25
 
25
- ```json
26
- // src/lib/labels/home/hero.json
27
- {
28
- "title": {
29
- "key": "home.hero.title",
30
- "defaultValue": {
31
- "ko": "혁신적인 솔루션",
32
- "en": "Innovative Solutions"
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
- // src/server/server.config.ts
42
- import { initLabelSync } from '@spfn/cms/server';
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
- beforeRoutes: async (app) => {
46
- await initLabelSync({ verbose: true });
47
- },
48
- } satisfies ServerConfig;
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 { getSection } from '@spfn/cms/server';
95
+ import { getLabel, format } from '@/labels';
57
96
 
58
97
  export default async function HomePage() {
59
- const { t } = await getSection('home');
60
- return <h1>{t('hero.title')}</h1>;
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
- **Client Component:**
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 { useSection } from '@spfn/cms/client';
137
+ import { setLocale } from '@spfn/cms/actions';
69
138
 
70
- export default function Nav() {
71
- const { t } = useSection('layout', { autoLoad: true });
72
- return <nav><a>{t('nav.about')}</a></nav>;
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
- ## Documentation
149
+ ## Key Features
77
150
 
78
- 📖 **[Full Documentation](../../docs/ecosystem/cms/index.md)**
151
+ ### 🎯 Type Safety
79
152
 
80
- - [Getting Started](../../docs/ecosystem/cms/getting-started.md) - Setup and configuration
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
- ## Configuration
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
- ```bash
90
- # .env.local
91
- SPFN_CMS_DEFAULT_LOCALE=ko
92
- SPFN_CMS_SUPPORTED_LOCALES=en,ko,ja
93
- SPFN_CMS_DETECT_BROWSER_LANGUAGE=true
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
- export { g as getLocale, a as getLocales, s as setLocale } from './actions-BEFWwQsh.js';
2
- export { L as LOCALE_COOKIE_KEY } from './locale.constants-BNkSdNP1.js';
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
- // src/server/helpers/locale.actions.ts
2
- import { cookies, headers } from "next/headers.js";
1
+ "use server";
3
2
 
4
- // src/server/config/cms.config.ts
5
- function getEnvVar(key, defaultValue) {
6
- return process.env[key] || defaultValue;
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(LOCALE_COOKIE_KEY, locale, {
84
- path: "/",
85
- maxAge: 60 * 60 * 24 * 365,
86
- // 1년
87
- sameSite: "lax"
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 getLocales() {
91
- const config = getCmsConfig();
92
- return config.locales;
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