@spfn/cms 0.1.0-alpha.9 → 0.2.0-beta.10
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 +436 -342
- package/dist/actions.d.ts +12 -6
- package/dist/actions.js +25 -10
- package/dist/actions.js.map +1 -1
- package/dist/config.d.ts +40 -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 +199 -20
- package/dist/index.js +202 -23
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +99 -81
- package/dist/server.js +842 -256
- package/dist/server.js.map +1 -1
- package/migrations/0000_medical_ozymandias.sql +54 -0
- package/migrations/meta/0000_snapshot.json +336 -0
- package/migrations/meta/_journal.json +13 -0
- package/package.json +56 -46
- package/dist/actions.d.ts.map +0 -1
- package/dist/client.d.ts +0 -138
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -62
- package/dist/client.js.map +0 -1
- package/dist/cms.config.d.ts +0 -77
- package/dist/cms.config.d.ts.map +0 -1
- package/dist/cms.config.js +0 -111
- package/dist/cms.config.js.map +0 -1
- package/dist/entities/cms-audit-logs.d.ts +0 -213
- package/dist/entities/cms-audit-logs.d.ts.map +0 -1
- package/dist/entities/cms-audit-logs.js +0 -103
- package/dist/entities/cms-audit-logs.js.map +0 -1
- package/dist/entities/cms-draft-cache.d.ts +0 -188
- package/dist/entities/cms-draft-cache.d.ts.map +0 -1
- package/dist/entities/cms-draft-cache.js +0 -112
- package/dist/entities/cms-draft-cache.js.map +0 -1
- package/dist/entities/cms-label-values.d.ts +0 -192
- package/dist/entities/cms-label-values.d.ts.map +0 -1
- package/dist/entities/cms-label-values.js +0 -105
- package/dist/entities/cms-label-values.js.map +0 -1
- package/dist/entities/cms-label-versions.d.ts +0 -207
- package/dist/entities/cms-label-versions.d.ts.map +0 -1
- package/dist/entities/cms-label-versions.js +0 -80
- package/dist/entities/cms-label-versions.js.map +0 -1
- package/dist/entities/cms-labels.d.ts +0 -189
- package/dist/entities/cms-labels.d.ts.map +0 -1
- package/dist/entities/cms-labels.js +0 -48
- package/dist/entities/cms-labels.js.map +0 -1
- package/dist/entities/cms-published-cache.d.ts +0 -199
- package/dist/entities/cms-published-cache.d.ts.map +0 -1
- package/dist/entities/cms-published-cache.js +0 -103
- package/dist/entities/cms-published-cache.js.map +0 -1
- package/dist/entities/index.d.ts +0 -10
- package/dist/entities/index.d.ts.map +0 -1
- package/dist/entities/index.js +0 -10
- package/dist/entities/index.js.map +0 -1
- package/dist/generators/index.d.ts +0 -19
- package/dist/generators/index.d.ts.map +0 -1
- package/dist/generators/index.js +0 -19
- package/dist/generators/index.js.map +0 -1
- package/dist/generators/label-sync-generator.d.ts +0 -33
- package/dist/generators/label-sync-generator.d.ts.map +0 -1
- package/dist/generators/label-sync-generator.js +0 -86
- package/dist/generators/label-sync-generator.js.map +0 -1
- package/dist/helpers/locale.actions.d.ts +0 -132
- package/dist/helpers/locale.actions.d.ts.map +0 -1
- package/dist/helpers/locale.actions.js +0 -210
- package/dist/helpers/locale.actions.js.map +0 -1
- package/dist/helpers/locale.constants.d.ts +0 -10
- package/dist/helpers/locale.constants.d.ts.map +0 -1
- package/dist/helpers/locale.constants.js +0 -10
- package/dist/helpers/locale.constants.js.map +0 -1
- package/dist/helpers/locale.d.ts +0 -17
- package/dist/helpers/locale.d.ts.map +0 -1
- package/dist/helpers/locale.js +0 -20
- package/dist/helpers/locale.js.map +0 -1
- package/dist/helpers/sync.d.ts +0 -41
- package/dist/helpers/sync.d.ts.map +0 -1
- package/dist/helpers/sync.js +0 -309
- package/dist/helpers/sync.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/init.d.ts +0 -31
- package/dist/init.d.ts.map +0 -1
- package/dist/init.js +0 -36
- package/dist/init.js.map +0 -1
- package/dist/labels/helpers.d.ts +0 -31
- package/dist/labels/helpers.d.ts.map +0 -1
- package/dist/labels/helpers.js +0 -60
- package/dist/labels/helpers.js.map +0 -1
- package/dist/labels/index.d.ts +0 -7
- package/dist/labels/index.d.ts.map +0 -1
- package/dist/labels/index.js +0 -7
- package/dist/labels/index.js.map +0 -1
- package/dist/repositories/cms-draft-cache.repository.d.ts +0 -62
- package/dist/repositories/cms-draft-cache.repository.d.ts.map +0 -1
- package/dist/repositories/cms-draft-cache.repository.js +0 -56
- package/dist/repositories/cms-draft-cache.repository.js.map +0 -1
- package/dist/repositories/cms-label-values.repository.d.ts +0 -32
- package/dist/repositories/cms-label-values.repository.d.ts.map +0 -1
- package/dist/repositories/cms-label-values.repository.js +0 -72
- package/dist/repositories/cms-label-values.repository.js.map +0 -1
- package/dist/repositories/cms-labels.repository.d.ts +0 -53
- package/dist/repositories/cms-labels.repository.d.ts.map +0 -1
- package/dist/repositories/cms-labels.repository.js +0 -77
- package/dist/repositories/cms-labels.repository.js.map +0 -1
- package/dist/repositories/cms-published-cache.repository.d.ts +0 -53
- package/dist/repositories/cms-published-cache.repository.d.ts.map +0 -1
- package/dist/repositories/cms-published-cache.repository.js +0 -54
- package/dist/repositories/cms-published-cache.repository.js.map +0 -1
- package/dist/repositories/index.d.ts +0 -8
- package/dist/repositories/index.d.ts.map +0 -1
- package/dist/repositories/index.js +0 -9
- package/dist/repositories/index.js.map +0 -1
- package/dist/routes/labels/[id]/contract.d.ts +0 -68
- package/dist/routes/labels/[id]/contract.d.ts.map +0 -1
- package/dist/routes/labels/[id]/contract.js +0 -84
- package/dist/routes/labels/[id]/contract.js.map +0 -1
- package/dist/routes/labels/[id]/index.d.ts +0 -10
- package/dist/routes/labels/[id]/index.d.ts.map +0 -1
- package/dist/routes/labels/[id]/index.js +0 -96
- package/dist/routes/labels/[id]/index.js.map +0 -1
- package/dist/routes/labels/by-key/[key]/contract.d.ts +0 -24
- package/dist/routes/labels/by-key/[key]/contract.d.ts.map +0 -1
- package/dist/routes/labels/by-key/[key]/contract.js +0 -28
- package/dist/routes/labels/by-key/[key]/contract.js.map +0 -1
- package/dist/routes/labels/by-key/[key]/index.d.ts +0 -8
- package/dist/routes/labels/by-key/[key]/index.d.ts.map +0 -1
- package/dist/routes/labels/by-key/[key]/index.js +0 -32
- package/dist/routes/labels/by-key/[key]/index.js.map +0 -1
- package/dist/routes/labels/contract.d.ts +0 -59
- package/dist/routes/labels/contract.d.ts.map +0 -1
- package/dist/routes/labels/contract.js +0 -75
- package/dist/routes/labels/contract.js.map +0 -1
- package/dist/routes/labels/index.d.ts +0 -10
- package/dist/routes/labels/index.d.ts.map +0 -1
- package/dist/routes/labels/index.js +0 -73
- package/dist/routes/labels/index.js.map +0 -1
- package/dist/routes/published-cache/contract.d.ts +0 -25
- package/dist/routes/published-cache/contract.d.ts.map +0 -1
- package/dist/routes/published-cache/contract.js +0 -35
- package/dist/routes/published-cache/contract.js.map +0 -1
- package/dist/routes/published-cache/index.d.ts +0 -8
- package/dist/routes/published-cache/index.d.ts.map +0 -1
- package/dist/routes/published-cache/index.js +0 -33
- package/dist/routes/published-cache/index.js.map +0 -1
- package/dist/routes/values/[labelId]/[version]/contract.d.ts +0 -29
- package/dist/routes/values/[labelId]/[version]/contract.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/[version]/contract.js +0 -33
- package/dist/routes/values/[labelId]/[version]/contract.js.map +0 -1
- package/dist/routes/values/[labelId]/[version]/index.d.ts +0 -8
- package/dist/routes/values/[labelId]/[version]/index.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/[version]/index.js +0 -45
- package/dist/routes/values/[labelId]/[version]/index.js.map +0 -1
- package/dist/routes/values/[labelId]/contract.d.ts +0 -38
- package/dist/routes/values/[labelId]/contract.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/contract.js +0 -59
- package/dist/routes/values/[labelId]/contract.js.map +0 -1
- package/dist/routes/values/[labelId]/index.d.ts +0 -8
- package/dist/routes/values/[labelId]/index.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/index.js +0 -42
- package/dist/routes/values/[labelId]/index.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/store.d.ts +0 -87
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -205
- package/dist/store.js.map +0 -1
- package/dist/types.d.ts +0 -74
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -7
- package/dist/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,490 +1,584 @@
|
|
|
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
|
-
- 🌐 **Multi-language support
|
|
10
|
-
- 🍪 **
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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
|
-
|
|
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
|
|
22
|
+
## Quick Start
|
|
31
23
|
|
|
32
|
-
|
|
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)
|
|
24
|
+
### 1. Define Labels & Configuration
|
|
39
25
|
|
|
40
|
-
|
|
26
|
+
```typescript
|
|
27
|
+
// labels.ts
|
|
28
|
+
import { defineLabelConfig, defineLabels, createCmsClient } from '@spfn/cms';
|
|
29
|
+
import { getLocale } from '@spfn/cms/actions';
|
|
41
30
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
// Configure locales
|
|
32
|
+
export const labelConfig = defineLabelConfig({
|
|
33
|
+
locales: ['en', 'ko'] as const,
|
|
34
|
+
defaultLocale: 'ko',
|
|
35
|
+
fallbackLocale: 'en'
|
|
36
|
+
});
|
|
45
37
|
|
|
46
|
-
|
|
38
|
+
// Define labels with nested structure
|
|
39
|
+
export const labelsDefinition = defineLabels({
|
|
40
|
+
home: {
|
|
41
|
+
hero: {
|
|
42
|
+
title: { en: "Welcome", ko: "환영합니다" },
|
|
43
|
+
subtitle: { en: "Start your journey", ko: "여정을 시작하세요" },
|
|
44
|
+
greeting: { en: "Hello {name}!", ko: "{name}님 안녕하세요!" }
|
|
45
|
+
},
|
|
46
|
+
cta: { en: "Get Started", ko: "시작하기" }
|
|
47
|
+
},
|
|
48
|
+
about: {
|
|
49
|
+
title: { en: "About Us", ko: "회사 소개" }
|
|
50
|
+
}
|
|
51
|
+
});
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
// Create client with API, getLabel, getLabels, and format
|
|
54
|
+
export const { api, getLabel, getLabels, format } = createCmsClient(
|
|
55
|
+
labelsDefinition,
|
|
56
|
+
{
|
|
57
|
+
defaultLocale: labelConfig.defaultLocale,
|
|
58
|
+
fallbackLocale: labelConfig.fallbackLocale,
|
|
59
|
+
getLocale: () => getLocale(labelConfig.defaultLocale),
|
|
60
|
+
}
|
|
61
|
+
);
|
|
51
62
|
```
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
### 2. Enable Auto-Sync
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
```typescript
|
|
67
|
+
// server.config.ts
|
|
68
|
+
import { defineServerConfig } from '@spfn/core/server';
|
|
69
|
+
import { syncLabels } from '@spfn/cms/server';
|
|
70
|
+
import { labelsDefinition } from './labels';
|
|
56
71
|
|
|
57
|
-
### 1. Create Label Files
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
// Option 1: Single definition
|
|
74
|
+
export default defineServerConfig()
|
|
75
|
+
.lifecycle({
|
|
76
|
+
afterInfrastructure: async () => {
|
|
77
|
+
await syncLabels(labelsDefinition);
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.build();
|
|
60
81
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
// Option 2: Multiple definitions (organized in separate files)
|
|
83
|
+
import { homeLabels } from './labels/home';
|
|
84
|
+
import { aboutLabels } from './labels/about';
|
|
85
|
+
|
|
86
|
+
export default defineServerConfig()
|
|
87
|
+
.lifecycle({
|
|
88
|
+
afterInfrastructure: async () => {
|
|
89
|
+
await syncLabels([homeLabels, aboutLabels]);
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.build();
|
|
69
93
|
```
|
|
70
94
|
|
|
71
|
-
|
|
95
|
+
### 3. Use in Your App
|
|
72
96
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
**Server Component (Single Section):**
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { getLabel, format } from '@/labels';
|
|
101
|
+
|
|
102
|
+
export default async function HomePage() {
|
|
103
|
+
// Single section - direct access without section name
|
|
104
|
+
const label = await getLabel('home');
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
<h1>{label.hero.title}</h1>
|
|
109
|
+
<p>{label.hero.subtitle}</p>
|
|
110
|
+
<button>{label.cta}</button>
|
|
111
|
+
|
|
112
|
+
{/* Template variables */}
|
|
113
|
+
<p>{format(label.hero.greeting, { name: 'John' })}</p>
|
|
114
|
+
{/* Output: "John님 안녕하세요!" */}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
89
117
|
}
|
|
90
118
|
```
|
|
91
119
|
|
|
92
|
-
**
|
|
120
|
+
**Server Component (Multiple Sections):**
|
|
93
121
|
|
|
94
|
-
```
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"en": "Your Best Partner for Business Growth"
|
|
108
|
-
}
|
|
109
|
-
}
|
|
122
|
+
```typescript
|
|
123
|
+
import { getLabels } from '@/labels';
|
|
124
|
+
|
|
125
|
+
export default async function MultiSectionPage() {
|
|
126
|
+
// Multiple sections - with section names as keys
|
|
127
|
+
const labels = await getLabels(['home', 'about']);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div>
|
|
131
|
+
<h1>{labels.home.hero.title}</h1>
|
|
132
|
+
<p>{labels.about.title}</p>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
110
135
|
}
|
|
111
136
|
```
|
|
112
137
|
|
|
113
|
-
**
|
|
138
|
+
**Locale Management:**
|
|
114
139
|
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
```typescript
|
|
141
|
+
'use client';
|
|
142
|
+
import { setLocale } from '@spfn/cms/actions';
|
|
143
|
+
|
|
144
|
+
export function LanguageSwitcher() {
|
|
145
|
+
return (
|
|
146
|
+
<div>
|
|
147
|
+
<button onClick={() => setLocale('ko')}>한국어</button>
|
|
148
|
+
<button onClick={() => setLocale('en')}>English</button>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
121
151
|
}
|
|
122
152
|
```
|
|
123
153
|
|
|
124
|
-
|
|
154
|
+
## Key Features
|
|
155
|
+
|
|
156
|
+
### 🎯 Type Safety
|
|
125
157
|
|
|
126
|
-
|
|
158
|
+
**Section Name Validation:**
|
|
127
159
|
|
|
128
160
|
```typescript
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
// ✅ Valid section names are enforced at compile time
|
|
162
|
+
const label = await getLabel('home'); // OK
|
|
163
|
+
await getLabel('homee'); // ❌ Compile error: 'homee' is not a valid section
|
|
164
|
+
|
|
165
|
+
// ✅ getLabel returns direct access (no section wrapper)
|
|
166
|
+
const homeLabel = await getLabel('home');
|
|
167
|
+
homeLabel.hero.title; // OK - direct access
|
|
168
|
+
homeLabel.cta; // OK
|
|
169
|
+
|
|
170
|
+
// ✅ getLabels requires array and returns sections with names
|
|
171
|
+
const labels = await getLabels(['home', 'about']);
|
|
172
|
+
labels.home.hero.title; // OK
|
|
173
|
+
labels.about.title; // OK
|
|
174
|
+
labels.contact.email; // ❌ Compile error: 'contact' was not requested
|
|
140
175
|
```
|
|
141
176
|
|
|
142
|
-
|
|
177
|
+
**Property Access Validation:**
|
|
143
178
|
|
|
144
|
-
|
|
179
|
+
```typescript
|
|
180
|
+
// Single section - direct access
|
|
181
|
+
const label = await getLabel('signup');
|
|
145
182
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
{
|
|
151
|
-
"name": "@spfn/cms:label-sync",
|
|
152
|
-
"enabled": true
|
|
153
|
-
}
|
|
154
|
-
]
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
```
|
|
183
|
+
// ✅ IDE autocomplete works perfectly
|
|
184
|
+
label.title; // OK - direct access
|
|
185
|
+
label.userName; // OK
|
|
186
|
+
label.email; // OK
|
|
158
187
|
|
|
159
|
-
|
|
188
|
+
// ❌ Typos are caught at compile time
|
|
189
|
+
label.titlee; // ❌ Compile error
|
|
190
|
+
label.userName; // ❌ Compile error (typo)
|
|
160
191
|
|
|
161
|
-
|
|
192
|
+
// Multiple sections - with section names
|
|
193
|
+
const labels = await getLabels(['home', 'about']);
|
|
194
|
+
labels.home.hero.title; // OK
|
|
195
|
+
labels.about.title; // OK
|
|
196
|
+
```
|
|
162
197
|
|
|
163
|
-
**
|
|
198
|
+
**API Distinction:**
|
|
164
199
|
|
|
165
200
|
```typescript
|
|
166
|
-
|
|
201
|
+
// getLabel: Single section, direct access
|
|
202
|
+
const signup = await getLabel('signup');
|
|
203
|
+
signup.title; // ✅ Direct access (cleaner!)
|
|
167
204
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
205
|
+
// getLabels: Multiple sections, section names as keys
|
|
206
|
+
const multi = await getLabels(['home', 'signup']);
|
|
207
|
+
multi.home.title; // ✅ With section name
|
|
208
|
+
multi.signup.userName; // ✅ With section name
|
|
173
209
|
```
|
|
174
210
|
|
|
175
|
-
|
|
211
|
+
### 🎨 Nested Structure
|
|
176
212
|
|
|
177
213
|
```typescript
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
defineLabels({
|
|
215
|
+
features: {
|
|
216
|
+
analytics: {
|
|
217
|
+
title: { en: "Analytics", ko: "분석" },
|
|
218
|
+
description: { en: "Track metrics", ko: "지표 추적" }
|
|
219
|
+
},
|
|
220
|
+
security: {
|
|
221
|
+
title: { en: "Security", ko: "보안" }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
181
224
|
});
|
|
182
|
-
|
|
225
|
+
|
|
226
|
+
// Single section - direct access with nesting
|
|
227
|
+
const label = await getLabel('features');
|
|
228
|
+
label.analytics.title; // "분석" (auto locale, direct access!)
|
|
229
|
+
label.analytics.description; // "지표 추적"
|
|
230
|
+
label.security.title; // "보안"
|
|
183
231
|
```
|
|
184
232
|
|
|
185
|
-
|
|
233
|
+
### 🔧 Template Variables
|
|
186
234
|
|
|
187
235
|
```typescript
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
defineLabels({
|
|
237
|
+
home: {
|
|
238
|
+
welcome: {
|
|
239
|
+
en: "Welcome {name}, you have {count} messages",
|
|
240
|
+
ko: "{name}님, {count}개의 메시지가 있습니다"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
193
244
|
|
|
194
|
-
|
|
245
|
+
const label = await getLabel('home');
|
|
246
|
+
const text = label.welcome; // Direct access!
|
|
195
247
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<a>{t('nav.about')}</a>
|
|
199
|
-
<a>{t('nav.services')}</a>
|
|
200
|
-
</nav>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
248
|
+
format(text, { name: "John", count: 5 });
|
|
249
|
+
// Output: "John님, 5개의 메시지가 있습니다"
|
|
203
250
|
```
|
|
204
251
|
|
|
205
|
-
|
|
252
|
+
### 🍪 Smart Locale Detection
|
|
206
253
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
```
|
|
254
|
+
Automatic locale detection with priority:
|
|
255
|
+
1. User's cookie (`cms-locale`)
|
|
256
|
+
2. Config's `defaultLocale`
|
|
257
|
+
3. Final fallback: `'en'`
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// User sets locale (saved to cookie)
|
|
261
|
+
await setLocale('ko');
|
|
216
262
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
- Inside JSON: `key` field defines the actual label key
|
|
263
|
+
// Automatically uses 'ko' locale
|
|
264
|
+
const label = await getLabel('home');
|
|
265
|
+
label.hero.title; // "환영합니다" (Korean)
|
|
221
266
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
267
|
+
// Switch to English
|
|
268
|
+
await setLocale('en');
|
|
269
|
+
const label2 = await getLabel('home');
|
|
270
|
+
label2.hero.title; // "Welcome" (English)
|
|
226
271
|
```
|
|
227
272
|
|
|
228
|
-
|
|
273
|
+
### 🔄 Auto-Sync
|
|
274
|
+
|
|
275
|
+
Labels synchronize automatically on server startup:
|
|
276
|
+
- ✅ Creates new labels
|
|
277
|
+
- ✅ Updates changed labels (deep equality check)
|
|
278
|
+
- ✅ Skips unchanged labels (performance)
|
|
279
|
+
- ✅ Rebuilds published cache
|
|
280
|
+
- ⚠️ Optionally removes orphaned labels
|
|
281
|
+
|
|
282
|
+
## API Reference
|
|
283
|
+
|
|
284
|
+
### createCmsClient()
|
|
285
|
+
|
|
286
|
+
Factory function to create CMS client with API, getLabel, getLabels, and format utilities.
|
|
229
287
|
|
|
230
288
|
```typescript
|
|
231
|
-
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
}
|
|
289
|
+
const { api, getLabel, getLabels, format } = createCmsClient(labelsDefinition, {
|
|
290
|
+
defaultLocale: labelConfig.defaultLocale,
|
|
291
|
+
fallbackLocale: labelConfig.fallbackLocale,
|
|
292
|
+
getLocale: () => getLocale(labelConfig.defaultLocale),
|
|
293
|
+
});
|
|
238
294
|
```
|
|
239
295
|
|
|
240
|
-
**
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
"defaultValue": "Welcome"
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
```
|
|
296
|
+
**Returns:**
|
|
297
|
+
- `api` - API client for CMS routes
|
|
298
|
+
- `getLabel(section)` - Fetch a single section (direct access)
|
|
299
|
+
- `getLabels(sections)` - Fetch multiple sections (with section names)
|
|
300
|
+
- `format(template, vars)` - Template variable substitution
|
|
249
301
|
|
|
250
|
-
|
|
251
|
-
```json
|
|
252
|
-
{
|
|
253
|
-
"welcome": {
|
|
254
|
-
"key": "home.welcome",
|
|
255
|
-
"defaultValue": {
|
|
256
|
-
"ko": "환영합니다",
|
|
257
|
-
"en": "Welcome",
|
|
258
|
-
"ja": "ようこそ"
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
```
|
|
302
|
+
### getLabel()
|
|
263
303
|
|
|
264
|
-
**
|
|
265
|
-
```json
|
|
266
|
-
{
|
|
267
|
-
"greeting": {
|
|
268
|
-
"key": "home.greeting",
|
|
269
|
-
"defaultValue": "Hello, {name}!"
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
```
|
|
304
|
+
Fetch a **single section** from published cache with direct access (no section name wrapper).
|
|
273
305
|
|
|
274
|
-
Usage:
|
|
275
306
|
```typescript
|
|
276
|
-
|
|
277
|
-
|
|
307
|
+
// Single section - direct access
|
|
308
|
+
const label = await getLabel('home');
|
|
309
|
+
label.hero.title; // Direct access without 'home.' prefix
|
|
310
|
+
label.cta;
|
|
278
311
|
```
|
|
279
312
|
|
|
280
|
-
|
|
313
|
+
**Use when:**
|
|
314
|
+
- You need labels from only ONE section
|
|
315
|
+
- You want cleaner, direct access to properties
|
|
316
|
+
- Most common use case for individual pages
|
|
281
317
|
|
|
282
|
-
|
|
318
|
+
**Returns:** Labels directly without section name wrapper
|
|
283
319
|
|
|
284
|
-
|
|
320
|
+
**Features:**
|
|
321
|
+
- Auto locale detection (cookie → defaultLocale → 'en')
|
|
322
|
+
- Direct property access (no section name)
|
|
323
|
+
- Type-safe with IDE autocomplete
|
|
324
|
+
- Merges published cache with defaults
|
|
285
325
|
|
|
286
|
-
|
|
287
|
-
# Default locale (default: 'en')
|
|
288
|
-
SPFN_CMS_DEFAULT_LOCALE=ko
|
|
326
|
+
### getLabels()
|
|
289
327
|
|
|
290
|
-
|
|
291
|
-
SPFN_CMS_SUPPORTED_LOCALES=en,ko,ja
|
|
328
|
+
Fetch **multiple sections** from published cache with section names as keys.
|
|
292
329
|
|
|
293
|
-
|
|
294
|
-
|
|
330
|
+
```typescript
|
|
331
|
+
// Multiple sections - with section names
|
|
332
|
+
const labels = await getLabels(['home', 'about']);
|
|
333
|
+
labels.home.hero.title; // Access via section name
|
|
334
|
+
labels.about.title;
|
|
295
335
|
```
|
|
296
336
|
|
|
297
|
-
|
|
337
|
+
**Use when:**
|
|
338
|
+
- You need labels from MULTIPLE sections
|
|
339
|
+
- You're building a page that uses multiple label groups
|
|
340
|
+
|
|
341
|
+
**Returns:** Object with section names as keys
|
|
342
|
+
|
|
343
|
+
**Features:**
|
|
344
|
+
- Auto locale detection (cookie → defaultLocale → 'en')
|
|
345
|
+
- Section filtering (only processes requested sections)
|
|
346
|
+
- Type-safe: only requested sections available
|
|
347
|
+
- Merges published cache with defaults
|
|
298
348
|
|
|
299
|
-
|
|
349
|
+
**Performance:**
|
|
350
|
+
- Only requested sections are processed (not entire labelsDefinition)
|
|
351
|
+
- 10x faster when requesting specific sections
|
|
352
|
+
- Reduces CPU and memory usage proportionally
|
|
353
|
+
|
|
354
|
+
### format()
|
|
355
|
+
|
|
356
|
+
Replace template variables in strings.
|
|
300
357
|
|
|
301
358
|
```typescript
|
|
302
|
-
|
|
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
|
-
});
|
|
359
|
+
format("Hello {name}!", { name: "John" }); // "Hello John!"
|
|
360
|
+
format("{count} items", { count: 5 }); // "5 items"
|
|
315
361
|
```
|
|
316
362
|
|
|
317
|
-
|
|
363
|
+
**Syntax:** `{variableName}` - Supports strings and numbers
|
|
364
|
+
|
|
365
|
+
### setLocale() / getLocale()
|
|
318
366
|
|
|
319
|
-
|
|
367
|
+
Server actions for locale management (cookie-based).
|
|
320
368
|
|
|
321
|
-
|
|
369
|
+
```typescript
|
|
370
|
+
// Set user's preferred locale
|
|
371
|
+
await setLocale('ko'); // Saves to 'cms-locale' cookie
|
|
322
372
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
373
|
+
// Get current locale
|
|
374
|
+
const locale = await getLocale(defaultLocale); // Returns: cookie → defaultLocale → 'en'
|
|
375
|
+
```
|
|
326
376
|
|
|
327
|
-
|
|
377
|
+
**Cookie settings:**
|
|
378
|
+
- Name: `cms-locale`
|
|
379
|
+
- Max age: 1 year
|
|
380
|
+
- HttpOnly, Secure (production), SameSite: lax
|
|
328
381
|
|
|
329
|
-
|
|
382
|
+
### syncLabels()
|
|
330
383
|
|
|
331
|
-
|
|
384
|
+
Synchronize labels to database (server-side only).
|
|
332
385
|
|
|
333
386
|
```typescript
|
|
334
|
-
|
|
335
|
-
|
|
387
|
+
await syncLabels(labelsDefinition, {
|
|
388
|
+
removeOrphaned: false, // Delete labels not in code
|
|
389
|
+
dryRun: false // Preview changes without applying
|
|
390
|
+
});
|
|
391
|
+
```
|
|
336
392
|
|
|
337
|
-
|
|
338
|
-
const locale = await getLocale();
|
|
393
|
+
**Returns:** `{ added, updated, removed, unchanged }`
|
|
339
394
|
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
```
|
|
395
|
+
## Admin API
|
|
343
396
|
|
|
344
|
-
|
|
345
|
-
// Client Component
|
|
346
|
-
'use client';
|
|
347
|
-
import { getLocale } from '@spfn/cms/actions';
|
|
348
|
-
import { useEffect, useState } from 'react';
|
|
397
|
+
CMS 라벨을 관리하기 위한 Admin API를 제공합니다. 섹션별 테이블 뷰로 라벨을 조회/수정/발행할 수 있습니다.
|
|
349
398
|
|
|
350
|
-
|
|
351
|
-
const [locale, setLocale] = useState('');
|
|
399
|
+
### Admin Routes
|
|
352
400
|
|
|
353
|
-
|
|
354
|
-
getLocale().then(setLocale);
|
|
355
|
-
}, []);
|
|
401
|
+
`cmsAppRouter`에 포함된 Admin 라우트들:
|
|
356
402
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
403
|
+
| Route | Method | Description |
|
|
404
|
+
|-------|--------|-------------|
|
|
405
|
+
| `getSectionLabels` | GET | 섹션의 모든 라벨 조회 (Draft/Published 상태 포함) |
|
|
406
|
+
| `saveSectionDraft` | POST | 섹션 라벨 일괄 Draft 저장 |
|
|
407
|
+
| `publishSection` | POST | 섹션 전체 발행 (Draft → Published) |
|
|
408
|
+
| `resetSectionDraft` | DELETE | 섹션 Draft 초기화 |
|
|
409
|
+
|
|
410
|
+
### Admin UI 구현 예제
|
|
360
411
|
|
|
361
|
-
**
|
|
412
|
+
**1. API Client 설정:**
|
|
362
413
|
|
|
363
414
|
```typescript
|
|
364
|
-
|
|
415
|
+
// labels.ts
|
|
416
|
+
import { createCmsClient } from '@spfn/cms';
|
|
365
417
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
418
|
+
export const { api: cmsApi } = createCmsClient(labelsDefinition, {
|
|
419
|
+
defaultLocale: labelConfig.defaultLocale,
|
|
420
|
+
fallbackLocale: labelConfig.fallbackLocale,
|
|
421
|
+
getLocale: () => getLocale(labelConfig.defaultLocale),
|
|
422
|
+
});
|
|
370
423
|
```
|
|
371
424
|
|
|
372
|
-
**
|
|
425
|
+
**2. 섹션 라벨 조회:**
|
|
373
426
|
|
|
374
427
|
```typescript
|
|
375
|
-
|
|
428
|
+
// 섹션의 모든 라벨을 Draft/Published 상태와 함께 조회
|
|
429
|
+
const data = await cmsApi.getSectionLabels.call({
|
|
430
|
+
params: { section: 'home' },
|
|
431
|
+
query: { locales: 'en,ko' }, // 콤마로 구분
|
|
432
|
+
});
|
|
376
433
|
|
|
377
|
-
|
|
434
|
+
// 반환값
|
|
435
|
+
{
|
|
436
|
+
section: 'home',
|
|
437
|
+
locales: ['en', 'ko'],
|
|
438
|
+
labels: [
|
|
439
|
+
{
|
|
440
|
+
id: 1,
|
|
441
|
+
key: 'home.hero.title',
|
|
442
|
+
defaultValue: { en: 'Welcome', ko: '환영합니다' },
|
|
443
|
+
draft: { en: 'Welcome!', ko: '환영합니다!' } | null,
|
|
444
|
+
published: { en: 'Welcome', ko: '환영합니다' } | null,
|
|
445
|
+
hasDraft: true
|
|
446
|
+
},
|
|
447
|
+
// ...
|
|
448
|
+
]
|
|
449
|
+
}
|
|
378
450
|
```
|
|
379
451
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
When `locale` is not specified, `getSection()` automatically uses the detected locale:
|
|
452
|
+
**3. Draft 저장:**
|
|
383
453
|
|
|
384
454
|
```typescript
|
|
385
|
-
|
|
455
|
+
// 수정된 라벨들을 Draft로 저장
|
|
456
|
+
await cmsApi.saveSectionDraft.call({
|
|
457
|
+
params: { section: 'home' },
|
|
458
|
+
body: {
|
|
459
|
+
labels: [
|
|
460
|
+
{ id: 1, values: { en: 'Welcome!', ko: '환영합니다!' } },
|
|
461
|
+
{ id: 2, values: { ko: '새로운 부제목' } }, // 특정 locale만 수정 가능
|
|
462
|
+
]
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
```
|
|
386
466
|
|
|
387
|
-
|
|
388
|
-
|
|
467
|
+
**4. 섹션 발행:**
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// Draft가 있는 모든 라벨을 Published로 발행
|
|
471
|
+
const result = await cmsApi.publishSection.call({
|
|
472
|
+
params: { section: 'home' },
|
|
473
|
+
body: { locales: ['en', 'ko'] },
|
|
474
|
+
});
|
|
389
475
|
|
|
390
|
-
//
|
|
391
|
-
|
|
476
|
+
// 반환값
|
|
477
|
+
{
|
|
478
|
+
published: 2, // 발행된 라벨 수
|
|
479
|
+
version: 3, // 최대 버전 번호
|
|
480
|
+
labels: ['home.hero.title', 'home.hero.subtitle'] // 발행된 라벨 키
|
|
481
|
+
}
|
|
392
482
|
```
|
|
393
483
|
|
|
394
|
-
|
|
484
|
+
**5. Draft 초기화:**
|
|
395
485
|
|
|
396
|
-
|
|
397
|
-
|
|
486
|
+
```typescript
|
|
487
|
+
// 섹션의 모든 Draft 삭제 (Published 값으로 복원)
|
|
488
|
+
await cmsApi.resetSectionDraft.call({
|
|
489
|
+
params: { section: 'home' },
|
|
490
|
+
});
|
|
491
|
+
```
|
|
398
492
|
|
|
399
|
-
|
|
493
|
+
### Workflow
|
|
400
494
|
|
|
401
495
|
```
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
│ - cms_labels │
|
|
416
|
-
│ - published_cache │ ⭐ Used by API
|
|
417
|
-
└─────────────────────┘
|
|
418
|
-
↓ HTTP API
|
|
419
|
-
┌─────────────────────┐
|
|
420
|
-
│ Application │
|
|
421
|
-
│ - getSection() │
|
|
422
|
-
│ - useSection() │
|
|
423
|
-
└─────────────────────┘
|
|
496
|
+
┌─────────────┐ saveSectionDraft ┌─────────────┐
|
|
497
|
+
│ Default │ ──────────────────────► │ Draft │
|
|
498
|
+
│ (코드 정의) │ │ (version:null)│
|
|
499
|
+
└─────────────┘ └──────┬──────┘
|
|
500
|
+
│
|
|
501
|
+
publishSection │
|
|
502
|
+
◄───────────────────────────────┘
|
|
503
|
+
│
|
|
504
|
+
▼
|
|
505
|
+
┌─────────────┐
|
|
506
|
+
│ Published │
|
|
507
|
+
│ (version:N) │
|
|
508
|
+
└─────────────┘
|
|
424
509
|
```
|
|
425
510
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
### Server-side API
|
|
511
|
+
**상태 우선순위:** Draft > Published > Default
|
|
429
512
|
|
|
430
|
-
|
|
431
|
-
- `getSections(sections, locale?)` - Get multiple sections (auto-detects locale if not specified)
|
|
432
|
-
- `initLabelSync(options?)` - Sync labels on server startup
|
|
513
|
+
### 상태 표시
|
|
433
514
|
|
|
434
|
-
|
|
515
|
+
| 상태 | 의미 | UI 표시 예 |
|
|
516
|
+
|------|------|-----------|
|
|
517
|
+
| Default | 코드에 정의된 기본값만 존재 | 회색 |
|
|
518
|
+
| Draft | 저장되었으나 미발행 | 노란색 |
|
|
519
|
+
| Published | 발행되어 실제 서비스에 반영 | 초록색 |
|
|
520
|
+
| Edited | UI에서 수정 중 (미저장) | 파란색 |
|
|
435
521
|
|
|
436
|
-
|
|
522
|
+
## Architecture
|
|
437
523
|
|
|
438
|
-
|
|
439
|
-
- `setLocale(locale)` - Set locale (saves to cookie)
|
|
440
|
-
- `getLocales()` - Get supported locale list
|
|
441
|
-
- `LOCALE_COOKIE_KEY` - Locale cookie key constant
|
|
524
|
+
### Database Schema
|
|
442
525
|
|
|
443
|
-
|
|
526
|
+
```
|
|
527
|
+
cms_labels (metadata)
|
|
528
|
+
├─ id, key, section, type, defaultValue
|
|
529
|
+
└─ publishedVersion
|
|
444
530
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
531
|
+
cms_label_values (actual content)
|
|
532
|
+
├─ labelId, version, locale, breakpoint
|
|
533
|
+
└─ value (JSONB)
|
|
448
534
|
|
|
449
|
-
|
|
535
|
+
cms_published_cache (performance)
|
|
536
|
+
├─ section, locale, content (JSONB)
|
|
537
|
+
└─ version (for cache invalidation)
|
|
538
|
+
```
|
|
450
539
|
|
|
451
|
-
|
|
452
|
-
- `useSections(sections)` - Multiple sections hook
|
|
453
|
-
- `useCmsStore()` - CMS store hook
|
|
454
|
-
- `cmsApi` - CMS API client
|
|
455
|
-
- `InitCms` - Client initialization component
|
|
540
|
+
### Query Flow
|
|
456
541
|
|
|
457
|
-
|
|
542
|
+
1. **getLabels()** → published_cache (single query, 5ms)
|
|
543
|
+
2. **Fallback** → bindLocale(defaults)
|
|
544
|
+
3. **Merge** → cache overrides defaults
|
|
545
|
+
4. **Return** → type-safe nested object
|
|
458
546
|
|
|
459
|
-
|
|
460
|
-
- `syncAll(sections, options?)` - Sync all sections
|
|
461
|
-
- `syncSection(definition, options?)` - Sync specific section
|
|
547
|
+
## Performance
|
|
462
548
|
|
|
463
|
-
|
|
549
|
+
- **Published cache:** 5ms (vs 87ms with JOINs) - 17x faster
|
|
550
|
+
- **N+1 prevention:** Bulk section queries with `inArray()`
|
|
551
|
+
- **Section filtering:** Only requested sections processed (10x faster for selective access)
|
|
552
|
+
- **Unchanged labels:** Skipped during sync (deep equality check)
|
|
553
|
+
- **Client caching:** Version-based invalidation
|
|
464
554
|
|
|
465
|
-
|
|
466
|
-
- `LabelSyncGenerator` - Generator class
|
|
555
|
+
### Example: Large Scale
|
|
467
556
|
|
|
468
|
-
|
|
557
|
+
```typescript
|
|
558
|
+
// 10 sections with 100 labels each = 1,000 total labels
|
|
559
|
+
const labelsDefinition = {
|
|
560
|
+
home: { /* 100 labels */ },
|
|
561
|
+
about: { /* 100 labels */ },
|
|
562
|
+
products: { /* 100 labels */ },
|
|
563
|
+
// ... 7 more sections
|
|
564
|
+
};
|
|
469
565
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
3. **Labels immediately available** via `getSection()` or `useSection()`
|
|
566
|
+
// Only request 'home' - direct access
|
|
567
|
+
const label = await getLabel('home');
|
|
473
568
|
|
|
474
|
-
|
|
569
|
+
// ✅ Performance: Processes only 100 labels (10% of total)
|
|
570
|
+
// ❌ Without filtering: Would process all 1,000 labels
|
|
571
|
+
```
|
|
475
572
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
573
|
+
**Benefits:**
|
|
574
|
+
- CPU usage: 10x reduction (100 vs 1,000 labels)
|
|
575
|
+
- Memory usage: 10x reduction
|
|
576
|
+
- Response time: Proportionally faster
|
|
479
577
|
|
|
480
|
-
|
|
481
|
-
echo '{"test": {"key": "layout.test", "defaultValue": "Test"}}' > src/cms/labels/layout/test.json
|
|
578
|
+
## Development Status
|
|
482
579
|
|
|
483
|
-
|
|
484
|
-
# ✅ Label sync completed
|
|
485
|
-
# Created: 1
|
|
486
|
-
```
|
|
580
|
+
This package is currently in alpha. APIs may change.
|
|
487
581
|
|
|
488
582
|
## License
|
|
489
583
|
|
|
490
|
-
MIT
|
|
584
|
+
MIT
|