servcraft 0.1.0 → 0.1.3
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/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Feature Flag Module
|
|
2
|
+
|
|
3
|
+
Feature flags for A/B testing, progressive rollouts, and feature management.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multiple Strategies** - Boolean, percentage, user list, user attributes, date range
|
|
8
|
+
- **User Targeting** - Target specific users or user segments
|
|
9
|
+
- **Overrides** - Per-user or per-session overrides
|
|
10
|
+
- **Analytics** - Track flag evaluations and usage statistics
|
|
11
|
+
- **Environment Support** - Different flags per environment
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
17
|
+
│ Feature Flag Service │
|
|
18
|
+
├──────────────────────────────────────────────────────────────┤
|
|
19
|
+
│ Flag CRUD │ Evaluation Engine │ Override Mgmt │ Stats │
|
|
20
|
+
└──────┬──────┴─────────┬───────────┴────────┬────────┴───┬────┘
|
|
21
|
+
│ │ │ │
|
|
22
|
+
▼ ▼ ▼ ▼
|
|
23
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ Prisma (Flags & Overrides) │
|
|
25
|
+
│ Redis (Statistics) │
|
|
26
|
+
└──────────────────────────────────────────────────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Strategies
|
|
30
|
+
|
|
31
|
+
| Strategy | Description | Use Case |
|
|
32
|
+
|----------|-------------|----------|
|
|
33
|
+
| `boolean` | Simple on/off | Kill switches |
|
|
34
|
+
| `percentage` | Percentage rollout | Gradual releases |
|
|
35
|
+
| `user-list` | Specific user IDs | Beta testers |
|
|
36
|
+
| `user-attribute` | User property rules | Segment targeting |
|
|
37
|
+
| `date-range` | Time-based activation | Scheduled features |
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Basic Setup
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { createFeatureFlagService } from 'servcraft/modules/feature-flag';
|
|
45
|
+
|
|
46
|
+
const flagService = createFeatureFlagService({
|
|
47
|
+
defaultEnvironment: 'production',
|
|
48
|
+
analytics: true,
|
|
49
|
+
cacheTtl: 300,
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Creating Flags
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Boolean flag
|
|
57
|
+
await flagService.createFlag({
|
|
58
|
+
key: 'new-dashboard',
|
|
59
|
+
name: 'New Dashboard',
|
|
60
|
+
description: 'Enable the redesigned dashboard',
|
|
61
|
+
strategy: 'boolean',
|
|
62
|
+
config: { value: false },
|
|
63
|
+
status: 'active',
|
|
64
|
+
environment: 'production',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Percentage rollout
|
|
68
|
+
await flagService.createFlag({
|
|
69
|
+
key: 'new-checkout',
|
|
70
|
+
name: 'New Checkout Flow',
|
|
71
|
+
strategy: 'percentage',
|
|
72
|
+
config: { percentage: 25 }, // 25% of users
|
|
73
|
+
status: 'active',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// User list (beta testers)
|
|
77
|
+
await flagService.createFlag({
|
|
78
|
+
key: 'beta-features',
|
|
79
|
+
name: 'Beta Features',
|
|
80
|
+
strategy: 'user-list',
|
|
81
|
+
config: { userIds: ['user-1', 'user-2', 'user-3'] },
|
|
82
|
+
status: 'active',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// User attributes
|
|
86
|
+
await flagService.createFlag({
|
|
87
|
+
key: 'premium-features',
|
|
88
|
+
name: 'Premium Features',
|
|
89
|
+
strategy: 'user-attribute',
|
|
90
|
+
config: {
|
|
91
|
+
userAttributes: [
|
|
92
|
+
{ attribute: 'plan', operator: 'in', value: ['pro', 'enterprise'] },
|
|
93
|
+
{ attribute: 'accountAge', operator: 'gte', value: 30 },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
status: 'active',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Date range
|
|
100
|
+
await flagService.createFlag({
|
|
101
|
+
key: 'holiday-theme',
|
|
102
|
+
name: 'Holiday Theme',
|
|
103
|
+
strategy: 'date-range',
|
|
104
|
+
config: {
|
|
105
|
+
dateRange: {
|
|
106
|
+
start: '2024-12-20T00:00:00Z',
|
|
107
|
+
end: '2025-01-02T23:59:59Z',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
status: 'active',
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Evaluating Flags
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Simple check
|
|
118
|
+
const isEnabled = await flagService.isEnabled('new-dashboard', {
|
|
119
|
+
userId: 'user-123',
|
|
120
|
+
environment: 'production',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Full evaluation with reason
|
|
124
|
+
const result = await flagService.evaluateFlag('new-checkout', {
|
|
125
|
+
userId: 'user-123',
|
|
126
|
+
sessionId: 'session-456',
|
|
127
|
+
environment: 'production',
|
|
128
|
+
userAttributes: {
|
|
129
|
+
plan: 'pro',
|
|
130
|
+
accountAge: 45,
|
|
131
|
+
country: 'US',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// {
|
|
135
|
+
// key: 'new-checkout',
|
|
136
|
+
// enabled: true,
|
|
137
|
+
// reason: 'Percentage rollout: 25%',
|
|
138
|
+
// strategy: 'percentage',
|
|
139
|
+
// evaluatedAt: Date
|
|
140
|
+
// }
|
|
141
|
+
|
|
142
|
+
// Evaluate multiple flags
|
|
143
|
+
const flags = await flagService.evaluateFlags(
|
|
144
|
+
['new-dashboard', 'new-checkout', 'beta-features'],
|
|
145
|
+
{ userId: 'user-123' }
|
|
146
|
+
);
|
|
147
|
+
// { 'new-dashboard': {...}, 'new-checkout': {...}, 'beta-features': {...} }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Managing Flags
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Update flag
|
|
154
|
+
await flagService.updateFlag('new-checkout', {
|
|
155
|
+
config: { percentage: 50 }, // Increase to 50%
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Get flag
|
|
159
|
+
const flag = await flagService.getFlag('new-checkout');
|
|
160
|
+
|
|
161
|
+
// List flags
|
|
162
|
+
const allFlags = await flagService.listFlags({
|
|
163
|
+
status: 'active',
|
|
164
|
+
environment: 'production',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Delete flag
|
|
168
|
+
await flagService.deleteFlag('old-feature');
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Overrides
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Set user override (for testing/support)
|
|
175
|
+
await flagService.setOverride(
|
|
176
|
+
'new-checkout',
|
|
177
|
+
'user-123',
|
|
178
|
+
'user',
|
|
179
|
+
true, // force enabled
|
|
180
|
+
new Date('2024-02-01') // expires
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Set session override
|
|
184
|
+
await flagService.setOverride('new-checkout', 'session-456', 'session', false);
|
|
185
|
+
|
|
186
|
+
// Remove override
|
|
187
|
+
await flagService.removeOverride('new-checkout', 'user-123');
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Statistics
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const stats = await flagService.getStats('new-checkout');
|
|
194
|
+
// {
|
|
195
|
+
// totalEvaluations: 10000,
|
|
196
|
+
// enabledCount: 2500,
|
|
197
|
+
// disabledCount: 7500,
|
|
198
|
+
// uniqueUsers: 5000,
|
|
199
|
+
// lastEvaluatedAt: Date
|
|
200
|
+
// }
|
|
201
|
+
|
|
202
|
+
// Get events for a flag
|
|
203
|
+
const events = await flagService.getEvents('new-checkout', 100);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Attribute Operators
|
|
207
|
+
|
|
208
|
+
| Operator | Description | Example |
|
|
209
|
+
|----------|-------------|---------|
|
|
210
|
+
| `eq` | Equals | `{ attribute: 'plan', operator: 'eq', value: 'pro' }` |
|
|
211
|
+
| `ne` | Not equals | `{ attribute: 'status', operator: 'ne', value: 'banned' }` |
|
|
212
|
+
| `in` | In array | `{ attribute: 'country', operator: 'in', value: ['US', 'CA'] }` |
|
|
213
|
+
| `nin` | Not in array | `{ attribute: 'role', operator: 'nin', value: ['guest'] }` |
|
|
214
|
+
| `gt` | Greater than | `{ attribute: 'age', operator: 'gt', value: 18 }` |
|
|
215
|
+
| `gte` | Greater or equal | `{ attribute: 'score', operator: 'gte', value: 100 }` |
|
|
216
|
+
| `lt` | Less than | `{ attribute: 'failedLogins', operator: 'lt', value: 5 }` |
|
|
217
|
+
| `lte` | Less or equal | `{ attribute: 'cart', operator: 'lte', value: 1000 }` |
|
|
218
|
+
| `contains` | String contains | `{ attribute: 'email', operator: 'contains', value: '@company.com' }` |
|
|
219
|
+
| `starts-with` | String starts with | `{ attribute: 'name', operator: 'starts-with', value: 'Admin' }` |
|
|
220
|
+
| `ends-with` | String ends with | `{ attribute: 'domain', operator: 'ends-with', value: '.edu' }` |
|
|
221
|
+
|
|
222
|
+
## Types
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
interface FeatureFlag {
|
|
226
|
+
key: string;
|
|
227
|
+
name: string;
|
|
228
|
+
description?: string;
|
|
229
|
+
strategy: 'boolean' | 'percentage' | 'user-list' | 'user-attribute' | 'date-range';
|
|
230
|
+
config: FlagConfig;
|
|
231
|
+
status: 'active' | 'disabled' | 'archived';
|
|
232
|
+
environment?: string;
|
|
233
|
+
tags?: string[];
|
|
234
|
+
createdBy?: string;
|
|
235
|
+
createdAt: Date;
|
|
236
|
+
updatedAt: Date;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface FlagEvaluationContext {
|
|
240
|
+
userId?: string;
|
|
241
|
+
sessionId?: string;
|
|
242
|
+
environment?: string;
|
|
243
|
+
userAttributes?: Record<string, string | number | boolean>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface FlagEvaluationResult {
|
|
247
|
+
key: string;
|
|
248
|
+
enabled: boolean;
|
|
249
|
+
reason: string;
|
|
250
|
+
strategy: string;
|
|
251
|
+
evaluatedAt: Date;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## React Integration Example
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Hook
|
|
259
|
+
function useFeatureFlag(key: string): boolean {
|
|
260
|
+
const [enabled, setEnabled] = useState(false);
|
|
261
|
+
const user = useUser();
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
flagService.isEnabled(key, {
|
|
265
|
+
userId: user.id,
|
|
266
|
+
userAttributes: {
|
|
267
|
+
plan: user.plan,
|
|
268
|
+
country: user.country,
|
|
269
|
+
},
|
|
270
|
+
}).then(setEnabled);
|
|
271
|
+
}, [key, user]);
|
|
272
|
+
|
|
273
|
+
return enabled;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Usage
|
|
277
|
+
function Dashboard() {
|
|
278
|
+
const showNewDashboard = useFeatureFlag('new-dashboard');
|
|
279
|
+
|
|
280
|
+
return showNewDashboard ? <NewDashboard /> : <OldDashboard />;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Best Practices
|
|
285
|
+
|
|
286
|
+
1. **Naming Convention** - Use kebab-case: `new-feature`, `beta-checkout`
|
|
287
|
+
2. **Short-lived Flags** - Remove flags after full rollout
|
|
288
|
+
3. **Default Off** - New flags should default to disabled
|
|
289
|
+
4. **Documentation** - Add descriptions to all flags
|
|
290
|
+
5. **Cleanup** - Archive unused flags regularly
|
|
291
|
+
6. **Testing** - Use overrides for testing specific states
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# I18n Module
|
|
2
|
+
|
|
3
|
+
Internationalization and localization support for multi-language applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Translation Management** - Load and manage translations from files or programmatically
|
|
8
|
+
- **Variable Interpolation** - Dynamic values in translations
|
|
9
|
+
- **Pluralization** - Proper plural forms using Intl.PluralRules
|
|
10
|
+
- **Date/Number/Currency Formatting** - Locale-aware formatting
|
|
11
|
+
- **Relative Time** - Human-readable relative time (e.g., "2 hours ago")
|
|
12
|
+
- **RTL Support** - Right-to-left language support
|
|
13
|
+
|
|
14
|
+
## Supported Locales (Built-in)
|
|
15
|
+
|
|
16
|
+
| Code | Language | Direction | Currency |
|
|
17
|
+
|------|----------|-----------|----------|
|
|
18
|
+
| `en` | English | LTR | USD |
|
|
19
|
+
| `fr` | Français | LTR | EUR |
|
|
20
|
+
| `es` | Español | LTR | EUR |
|
|
21
|
+
| `de` | Deutsch | LTR | EUR |
|
|
22
|
+
| `ar` | العربية | RTL | SAR |
|
|
23
|
+
| `zh` | 中文 | LTR | CNY |
|
|
24
|
+
| `ja` | 日本語 | LTR | JPY |
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Configuration
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { I18nService } from 'servcraft/modules/i18n';
|
|
32
|
+
|
|
33
|
+
const i18n = new I18nService({
|
|
34
|
+
defaultLocale: 'en',
|
|
35
|
+
supportedLocales: ['en', 'fr', 'es', 'de'],
|
|
36
|
+
fallbackLocale: 'en',
|
|
37
|
+
translationsDir: './locales',
|
|
38
|
+
cache: true,
|
|
39
|
+
debug: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Load translations from files
|
|
43
|
+
await i18n.loadTranslations('en', 'common');
|
|
44
|
+
await i18n.loadTranslations('fr', 'common');
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### File Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
locales/
|
|
51
|
+
├── en/
|
|
52
|
+
│ ├── common.json
|
|
53
|
+
│ └── errors.json
|
|
54
|
+
├── fr/
|
|
55
|
+
│ ├── common.json
|
|
56
|
+
│ └── errors.json
|
|
57
|
+
└── es/
|
|
58
|
+
├── common.json
|
|
59
|
+
└── errors.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Translation File Format
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
// locales/en/common.json
|
|
66
|
+
{
|
|
67
|
+
"welcome": "Welcome, {{name}}!",
|
|
68
|
+
"items": "You have {{count}} item{s}",
|
|
69
|
+
"user": {
|
|
70
|
+
"profile": {
|
|
71
|
+
"title": "User Profile",
|
|
72
|
+
"settings": "Settings"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Basic Translation
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// Simple translation
|
|
82
|
+
const text = i18n.t('welcome');
|
|
83
|
+
// "Welcome, {{name}}!"
|
|
84
|
+
|
|
85
|
+
// With locale
|
|
86
|
+
const frText = i18n.t('welcome', { locale: 'fr' });
|
|
87
|
+
|
|
88
|
+
// With namespace
|
|
89
|
+
const errorText = i18n.t('notFound', { namespace: 'errors' });
|
|
90
|
+
|
|
91
|
+
// Nested keys
|
|
92
|
+
const title = i18n.t('user.profile.title');
|
|
93
|
+
// "User Profile"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Variable Interpolation
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const welcome = i18n.t('welcome', {
|
|
100
|
+
variables: { name: 'John' }
|
|
101
|
+
});
|
|
102
|
+
// "Welcome, John!"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Pluralization
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Translation: "You have {{count}} item{s}"
|
|
109
|
+
const items1 = i18n.t('items', { count: 1 });
|
|
110
|
+
// "You have 1 item"
|
|
111
|
+
|
|
112
|
+
const items5 = i18n.t('items', { count: 5 });
|
|
113
|
+
// "You have 5 items"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Formatting
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// Date formatting
|
|
120
|
+
const date = i18n.formatDate(new Date(), 'en', {
|
|
121
|
+
year: 'numeric',
|
|
122
|
+
month: 'long',
|
|
123
|
+
day: 'numeric',
|
|
124
|
+
});
|
|
125
|
+
// "December 20, 2024"
|
|
126
|
+
|
|
127
|
+
// Number formatting
|
|
128
|
+
const number = i18n.formatNumber(1234567.89, 'de');
|
|
129
|
+
// "1.234.567,89"
|
|
130
|
+
|
|
131
|
+
// Currency formatting
|
|
132
|
+
const price = i18n.formatCurrency(99.99, 'en', 'USD');
|
|
133
|
+
// "$99.99"
|
|
134
|
+
|
|
135
|
+
const euroPrice = i18n.formatCurrency(99.99, 'fr', 'EUR');
|
|
136
|
+
// "99,99 €"
|
|
137
|
+
|
|
138
|
+
// Relative time
|
|
139
|
+
const relative = i18n.formatRelativeTime(new Date(Date.now() - 3600000), 'en');
|
|
140
|
+
// "1 hour ago"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Programmatic Translations
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Add translations at runtime
|
|
147
|
+
i18n.addTranslations({
|
|
148
|
+
locale: 'en',
|
|
149
|
+
namespace: 'common',
|
|
150
|
+
data: {
|
|
151
|
+
newFeature: 'Check out our new feature!',
|
|
152
|
+
button: {
|
|
153
|
+
save: 'Save',
|
|
154
|
+
cancel: 'Cancel',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Locale Information
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Get locale info
|
|
164
|
+
const info = i18n.getLocaleInfo('ar');
|
|
165
|
+
// {
|
|
166
|
+
// code: 'ar',
|
|
167
|
+
// name: 'العربية',
|
|
168
|
+
// englishName: 'Arabic',
|
|
169
|
+
// direction: 'rtl',
|
|
170
|
+
// dateFormat: 'DD/MM/YYYY',
|
|
171
|
+
// currency: 'SAR'
|
|
172
|
+
// }
|
|
173
|
+
|
|
174
|
+
// Check if locale is supported
|
|
175
|
+
if (i18n.isLocaleSupported('fr')) {
|
|
176
|
+
// Use French
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Get supported locales
|
|
180
|
+
const locales = i18n.getSupportedLocales();
|
|
181
|
+
// ['en', 'fr', 'es', 'de']
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Translation Metadata
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// Get translation statistics
|
|
188
|
+
const metadata = await i18n.getTranslationMetadata('fr', 'common');
|
|
189
|
+
// {
|
|
190
|
+
// totalKeys: 50,
|
|
191
|
+
// translatedKeys: 45,
|
|
192
|
+
// completionPercentage: 90
|
|
193
|
+
// }
|
|
194
|
+
|
|
195
|
+
// Find missing translations
|
|
196
|
+
const missing = i18n.getMissingTranslations('en', 'fr', 'common');
|
|
197
|
+
// ['newFeature', 'button.help']
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Cache Management
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// Clear translation cache
|
|
204
|
+
i18n.clearCache();
|
|
205
|
+
|
|
206
|
+
// Export translations for backup/editing
|
|
207
|
+
const translations = i18n.exportTranslations('en', 'common');
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Configuration Types
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
interface I18nConfig {
|
|
214
|
+
defaultLocale: string;
|
|
215
|
+
supportedLocales: string[];
|
|
216
|
+
fallbackLocale?: string;
|
|
217
|
+
translationsDir?: string;
|
|
218
|
+
cache?: boolean;
|
|
219
|
+
debug?: boolean;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface TranslationOptions {
|
|
223
|
+
locale?: string;
|
|
224
|
+
namespace?: string;
|
|
225
|
+
variables?: Record<string, string | number>;
|
|
226
|
+
count?: number;
|
|
227
|
+
defaultValue?: string;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Fastify Integration
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// Detect locale from request
|
|
235
|
+
fastify.addHook('preHandler', async (request) => {
|
|
236
|
+
const acceptLanguage = request.headers['accept-language'];
|
|
237
|
+
const locale = parseAcceptLanguage(acceptLanguage) || 'en';
|
|
238
|
+
|
|
239
|
+
if (i18n.isLocaleSupported(locale)) {
|
|
240
|
+
request.locale = locale;
|
|
241
|
+
} else {
|
|
242
|
+
request.locale = 'en';
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Use in routes
|
|
247
|
+
fastify.get('/api/welcome', async (request, reply) => {
|
|
248
|
+
const message = i18n.t('welcome', {
|
|
249
|
+
locale: request.locale,
|
|
250
|
+
variables: { name: request.user?.name },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return { message };
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## React Integration Example
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// Create hook
|
|
261
|
+
function useTranslation(namespace = 'common') {
|
|
262
|
+
const locale = useLocale();
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
t: (key: string, options?: TranslationOptions) =>
|
|
266
|
+
i18n.t(key, { locale, namespace, ...options }),
|
|
267
|
+
formatDate: (date: Date, options?: DateFormatOptions) =>
|
|
268
|
+
i18n.formatDate(date, locale, options),
|
|
269
|
+
formatCurrency: (value: number, currency?: string) =>
|
|
270
|
+
i18n.formatCurrency(value, locale, currency),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Usage
|
|
275
|
+
function Welcome({ user }) {
|
|
276
|
+
const { t, formatCurrency } = useTranslation();
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div>
|
|
280
|
+
<h1>{t('welcome', { variables: { name: user.name } })}</h1>
|
|
281
|
+
<p>{t('balance')}: {formatCurrency(user.balance)}</p>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Best Practices
|
|
288
|
+
|
|
289
|
+
1. **Key Naming** - Use dot notation for namespacing: `user.profile.title`
|
|
290
|
+
2. **Variables** - Use meaningful variable names: `{{userName}}` not `{{n}}`
|
|
291
|
+
3. **Fallback** - Always configure a fallback locale
|
|
292
|
+
4. **Lazy Loading** - Load translations on demand for large apps
|
|
293
|
+
5. **Missing Keys** - Log missing translations in development
|
|
294
|
+
6. **RTL** - Test with RTL languages early in development
|