servcraft 0.1.0 → 0.1.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 (216) hide show
  1. package/.claude/settings.local.json +29 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/README.md +1070 -1
  9. package/dist/cli/index.cjs +2026 -2168
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +2026 -2168
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.cjs +595 -616
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +114 -52
  16. package/dist/index.d.ts +114 -52
  17. package/dist/index.js +595 -616
  18. package/dist/index.js.map +1 -1
  19. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  20. package/docs/DATABASE_MULTI_ORM.md +399 -0
  21. package/docs/PHASE1_BREAKDOWN.md +346 -0
  22. package/docs/PROGRESS.md +550 -0
  23. package/docs/modules/ANALYTICS.md +226 -0
  24. package/docs/modules/API-VERSIONING.md +252 -0
  25. package/docs/modules/AUDIT.md +192 -0
  26. package/docs/modules/AUTH.md +431 -0
  27. package/docs/modules/CACHE.md +346 -0
  28. package/docs/modules/EMAIL.md +254 -0
  29. package/docs/modules/FEATURE-FLAG.md +291 -0
  30. package/docs/modules/I18N.md +294 -0
  31. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  32. package/docs/modules/MFA.md +266 -0
  33. package/docs/modules/NOTIFICATION.md +311 -0
  34. package/docs/modules/OAUTH.md +237 -0
  35. package/docs/modules/PAYMENT.md +804 -0
  36. package/docs/modules/QUEUE.md +540 -0
  37. package/docs/modules/RATE-LIMIT.md +339 -0
  38. package/docs/modules/SEARCH.md +288 -0
  39. package/docs/modules/SECURITY.md +327 -0
  40. package/docs/modules/SESSION.md +382 -0
  41. package/docs/modules/SWAGGER.md +305 -0
  42. package/docs/modules/UPLOAD.md +296 -0
  43. package/docs/modules/USER.md +505 -0
  44. package/docs/modules/VALIDATION.md +294 -0
  45. package/docs/modules/WEBHOOK.md +270 -0
  46. package/docs/modules/WEBSOCKET.md +691 -0
  47. package/package.json +53 -38
  48. package/prisma/schema.prisma +395 -1
  49. package/src/cli/commands/add-module.ts +520 -87
  50. package/src/cli/commands/db.ts +3 -4
  51. package/src/cli/commands/docs.ts +256 -6
  52. package/src/cli/commands/generate.ts +12 -19
  53. package/src/cli/commands/init.ts +384 -214
  54. package/src/cli/index.ts +0 -4
  55. package/src/cli/templates/repository.ts +6 -1
  56. package/src/cli/templates/routes.ts +6 -21
  57. package/src/cli/utils/docs-generator.ts +6 -7
  58. package/src/cli/utils/env-manager.ts +717 -0
  59. package/src/cli/utils/field-parser.ts +16 -7
  60. package/src/cli/utils/interactive-prompt.ts +223 -0
  61. package/src/cli/utils/template-manager.ts +346 -0
  62. package/src/config/database.config.ts +183 -0
  63. package/src/config/env.ts +0 -10
  64. package/src/config/index.ts +0 -14
  65. package/src/core/server.ts +1 -1
  66. package/src/database/adapters/mongoose.adapter.ts +132 -0
  67. package/src/database/adapters/prisma.adapter.ts +118 -0
  68. package/src/database/connection.ts +190 -0
  69. package/src/database/interfaces/database.interface.ts +85 -0
  70. package/src/database/interfaces/index.ts +7 -0
  71. package/src/database/interfaces/repository.interface.ts +129 -0
  72. package/src/database/models/mongoose/index.ts +7 -0
  73. package/src/database/models/mongoose/payment.schema.ts +347 -0
  74. package/src/database/models/mongoose/user.schema.ts +154 -0
  75. package/src/database/prisma.ts +1 -4
  76. package/src/database/redis.ts +101 -0
  77. package/src/database/repositories/mongoose/index.ts +7 -0
  78. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  79. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  80. package/src/database/seed.ts +6 -1
  81. package/src/index.ts +9 -20
  82. package/src/middleware/security.ts +2 -6
  83. package/src/modules/analytics/analytics.routes.ts +80 -0
  84. package/src/modules/analytics/analytics.service.ts +364 -0
  85. package/src/modules/analytics/index.ts +18 -0
  86. package/src/modules/analytics/types.ts +180 -0
  87. package/src/modules/api-versioning/index.ts +15 -0
  88. package/src/modules/api-versioning/types.ts +86 -0
  89. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  90. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  91. package/src/modules/api-versioning/versioning.service.ts +189 -0
  92. package/src/modules/audit/audit.repository.ts +206 -0
  93. package/src/modules/audit/audit.service.ts +27 -59
  94. package/src/modules/auth/auth.controller.ts +2 -2
  95. package/src/modules/auth/auth.middleware.ts +3 -9
  96. package/src/modules/auth/auth.routes.ts +10 -107
  97. package/src/modules/auth/auth.service.ts +126 -23
  98. package/src/modules/auth/index.ts +3 -4
  99. package/src/modules/cache/cache.service.ts +367 -0
  100. package/src/modules/cache/index.ts +10 -0
  101. package/src/modules/cache/types.ts +44 -0
  102. package/src/modules/email/email.service.ts +3 -10
  103. package/src/modules/email/templates.ts +2 -8
  104. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  105. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  106. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  107. package/src/modules/feature-flag/index.ts +20 -0
  108. package/src/modules/feature-flag/types.ts +192 -0
  109. package/src/modules/i18n/i18n.middleware.ts +186 -0
  110. package/src/modules/i18n/i18n.routes.ts +191 -0
  111. package/src/modules/i18n/i18n.service.ts +456 -0
  112. package/src/modules/i18n/index.ts +18 -0
  113. package/src/modules/i18n/types.ts +118 -0
  114. package/src/modules/media-processing/index.ts +17 -0
  115. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  116. package/src/modules/media-processing/media-processing.service.ts +245 -0
  117. package/src/modules/media-processing/types.ts +156 -0
  118. package/src/modules/mfa/index.ts +20 -0
  119. package/src/modules/mfa/mfa.repository.ts +206 -0
  120. package/src/modules/mfa/mfa.routes.ts +595 -0
  121. package/src/modules/mfa/mfa.service.ts +572 -0
  122. package/src/modules/mfa/totp.ts +150 -0
  123. package/src/modules/mfa/types.ts +57 -0
  124. package/src/modules/notification/index.ts +20 -0
  125. package/src/modules/notification/notification.repository.ts +356 -0
  126. package/src/modules/notification/notification.service.ts +483 -0
  127. package/src/modules/notification/types.ts +119 -0
  128. package/src/modules/oauth/index.ts +20 -0
  129. package/src/modules/oauth/oauth.repository.ts +219 -0
  130. package/src/modules/oauth/oauth.routes.ts +446 -0
  131. package/src/modules/oauth/oauth.service.ts +293 -0
  132. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  133. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  134. package/src/modules/oauth/providers/github.provider.ts +248 -0
  135. package/src/modules/oauth/providers/google.provider.ts +189 -0
  136. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  137. package/src/modules/oauth/types.ts +94 -0
  138. package/src/modules/payment/index.ts +19 -0
  139. package/src/modules/payment/payment.repository.ts +733 -0
  140. package/src/modules/payment/payment.routes.ts +390 -0
  141. package/src/modules/payment/payment.service.ts +354 -0
  142. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  143. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  144. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  145. package/src/modules/payment/types.ts +140 -0
  146. package/src/modules/queue/cron.ts +438 -0
  147. package/src/modules/queue/index.ts +87 -0
  148. package/src/modules/queue/queue.routes.ts +600 -0
  149. package/src/modules/queue/queue.service.ts +842 -0
  150. package/src/modules/queue/types.ts +222 -0
  151. package/src/modules/queue/workers.ts +366 -0
  152. package/src/modules/rate-limit/index.ts +59 -0
  153. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  154. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  155. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  156. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  157. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  158. package/src/modules/rate-limit/types.ts +153 -0
  159. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  160. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  161. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  162. package/src/modules/search/index.ts +21 -0
  163. package/src/modules/search/search.service.ts +234 -0
  164. package/src/modules/search/types.ts +214 -0
  165. package/src/modules/security/index.ts +40 -0
  166. package/src/modules/security/sanitize.ts +223 -0
  167. package/src/modules/security/security-audit.service.ts +388 -0
  168. package/src/modules/security/security.middleware.ts +398 -0
  169. package/src/modules/session/index.ts +3 -0
  170. package/src/modules/session/session.repository.ts +159 -0
  171. package/src/modules/session/session.service.ts +340 -0
  172. package/src/modules/session/types.ts +38 -0
  173. package/src/modules/swagger/index.ts +7 -1
  174. package/src/modules/swagger/schema-builder.ts +16 -4
  175. package/src/modules/swagger/swagger.service.ts +9 -10
  176. package/src/modules/swagger/types.ts +0 -2
  177. package/src/modules/upload/index.ts +14 -0
  178. package/src/modules/upload/types.ts +83 -0
  179. package/src/modules/upload/upload.repository.ts +199 -0
  180. package/src/modules/upload/upload.routes.ts +311 -0
  181. package/src/modules/upload/upload.service.ts +448 -0
  182. package/src/modules/user/index.ts +3 -3
  183. package/src/modules/user/user.controller.ts +15 -9
  184. package/src/modules/user/user.repository.ts +237 -113
  185. package/src/modules/user/user.routes.ts +39 -164
  186. package/src/modules/user/user.service.ts +4 -3
  187. package/src/modules/validation/validator.ts +12 -17
  188. package/src/modules/webhook/index.ts +91 -0
  189. package/src/modules/webhook/retry.ts +196 -0
  190. package/src/modules/webhook/signature.ts +135 -0
  191. package/src/modules/webhook/types.ts +181 -0
  192. package/src/modules/webhook/webhook.repository.ts +358 -0
  193. package/src/modules/webhook/webhook.routes.ts +442 -0
  194. package/src/modules/webhook/webhook.service.ts +457 -0
  195. package/src/modules/websocket/features.ts +504 -0
  196. package/src/modules/websocket/index.ts +106 -0
  197. package/src/modules/websocket/middlewares.ts +298 -0
  198. package/src/modules/websocket/types.ts +181 -0
  199. package/src/modules/websocket/websocket.service.ts +692 -0
  200. package/src/utils/errors.ts +7 -0
  201. package/src/utils/pagination.ts +4 -1
  202. package/tests/helpers/db-check.ts +79 -0
  203. package/tests/integration/auth-redis.test.ts +94 -0
  204. package/tests/integration/cache-redis.test.ts +387 -0
  205. package/tests/integration/mongoose-repositories.test.ts +410 -0
  206. package/tests/integration/payment-prisma.test.ts +637 -0
  207. package/tests/integration/queue-bullmq.test.ts +417 -0
  208. package/tests/integration/user-prisma.test.ts +441 -0
  209. package/tests/integration/websocket-socketio.test.ts +552 -0
  210. package/tests/setup.ts +11 -9
  211. package/vitest.config.ts +3 -8
  212. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  213. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  216. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,192 @@
1
+ export type FlagStatus = 'enabled' | 'disabled';
2
+ export type FlagStrategy = 'boolean' | 'percentage' | 'user-list' | 'user-attribute' | 'date-range';
3
+ export type FlagEnvironment = 'development' | 'staging' | 'production' | 'test';
4
+
5
+ export interface FeatureFlag {
6
+ /** Unique flag key */
7
+ key: string;
8
+ /** Display name */
9
+ name: string;
10
+ /** Description */
11
+ description?: string;
12
+ /** Status */
13
+ status: FlagStatus;
14
+ /** Rollout strategy */
15
+ strategy: FlagStrategy;
16
+ /** Strategy configuration */
17
+ config: FlagConfig;
18
+ /** Environment */
19
+ environment?: FlagEnvironment;
20
+ /** Created by user ID */
21
+ createdBy?: string;
22
+ /** Created at timestamp */
23
+ createdAt: Date;
24
+ /** Updated at timestamp */
25
+ updatedAt: Date;
26
+ /** Tags for organization */
27
+ tags?: string[];
28
+ }
29
+
30
+ export interface FlagConfig {
31
+ /** Boolean value for boolean strategy */
32
+ value?: boolean;
33
+ /** Percentage (0-100) for percentage rollout */
34
+ percentage?: number;
35
+ /** List of user IDs for user-list strategy */
36
+ userIds?: string[];
37
+ /** User attribute rules */
38
+ userAttributes?: UserAttributeRule[];
39
+ /** Date range for time-based flags */
40
+ dateRange?: {
41
+ start: Date;
42
+ end: Date;
43
+ };
44
+ /** Custom data */
45
+ customData?: Record<string, unknown>;
46
+ }
47
+
48
+ export interface UserAttributeRule {
49
+ /** Attribute key (e.g., 'role', 'country', 'plan') */
50
+ attribute: string;
51
+ /** Operator */
52
+ operator:
53
+ | 'eq'
54
+ | 'ne'
55
+ | 'in'
56
+ | 'nin'
57
+ | 'gt'
58
+ | 'gte'
59
+ | 'lt'
60
+ | 'lte'
61
+ | 'contains'
62
+ | 'starts-with'
63
+ | 'ends-with';
64
+ /** Value to compare */
65
+ value: string | number | string[] | number[];
66
+ }
67
+
68
+ export interface FlagEvaluationContext {
69
+ /** User ID */
70
+ userId?: string;
71
+ /** User attributes */
72
+ userAttributes?: Record<string, string | number | boolean>;
73
+ /** Environment */
74
+ environment?: FlagEnvironment;
75
+ /** Session ID for consistent percentage rollout */
76
+ sessionId?: string;
77
+ /** Custom context data */
78
+ customData?: Record<string, unknown>;
79
+ }
80
+
81
+ export interface FlagEvaluationResult {
82
+ /** Flag key */
83
+ key: string;
84
+ /** Is flag enabled for this context */
85
+ enabled: boolean;
86
+ /** Reason for the decision */
87
+ reason: string;
88
+ /** Variant (for A/B testing) */
89
+ variant?: string;
90
+ /** Strategy used */
91
+ strategy: FlagStrategy;
92
+ /** Evaluation timestamp */
93
+ evaluatedAt: Date;
94
+ }
95
+
96
+ export interface FlagVariant {
97
+ /** Variant key */
98
+ key: string;
99
+ /** Display name */
100
+ name: string;
101
+ /** Weight for distribution (0-100) */
102
+ weight: number;
103
+ /** Variant configuration */
104
+ config?: Record<string, unknown>;
105
+ }
106
+
107
+ export interface FlagStats {
108
+ /** Total evaluations */
109
+ totalEvaluations: number;
110
+ /** Enabled evaluations */
111
+ enabledCount: number;
112
+ /** Disabled evaluations */
113
+ disabledCount: number;
114
+ /** Unique users */
115
+ uniqueUsers: number;
116
+ /** Evaluations by variant */
117
+ variantDistribution?: Record<string, number>;
118
+ /** Last evaluation */
119
+ lastEvaluatedAt?: Date;
120
+ }
121
+
122
+ export interface FlagOverride {
123
+ /** User ID or session ID */
124
+ targetId: string;
125
+ /** Target type */
126
+ targetType: 'user' | 'session';
127
+ /** Override value */
128
+ enabled: boolean;
129
+ /** Expiration date */
130
+ expiresAt?: Date;
131
+ /** Created at */
132
+ createdAt: Date;
133
+ }
134
+
135
+ export interface FlagEvent {
136
+ /** Event type */
137
+ type: 'created' | 'updated' | 'deleted' | 'evaluated' | 'override-set' | 'override-removed';
138
+ /** Flag key */
139
+ flagKey: string;
140
+ /** User ID */
141
+ userId?: string;
142
+ /** Event data */
143
+ data?: Record<string, unknown>;
144
+ /** Timestamp */
145
+ timestamp: Date;
146
+ }
147
+
148
+ export interface FeatureFlagConfig {
149
+ /** Default environment */
150
+ defaultEnvironment?: FlagEnvironment;
151
+ /** Enable analytics */
152
+ analytics?: boolean;
153
+ /** Cache TTL in seconds */
154
+ cacheTtl?: number;
155
+ /** Redis URL for distributed caching */
156
+ redisUrl?: string;
157
+ }
158
+
159
+ export interface FlagListFilters {
160
+ /** Filter by status */
161
+ status?: FlagStatus;
162
+ /** Filter by environment */
163
+ environment?: FlagEnvironment;
164
+ /** Filter by tags */
165
+ tags?: string[];
166
+ /** Search by name or description */
167
+ search?: string;
168
+ }
169
+
170
+ export interface ABTestConfig {
171
+ /** Flag key */
172
+ flagKey: string;
173
+ /** Variants */
174
+ variants: FlagVariant[];
175
+ /** Traffic allocation percentage */
176
+ trafficAllocation: number;
177
+ /** Conversion goal event */
178
+ conversionGoal?: string;
179
+ }
180
+
181
+ export interface ABTestResult {
182
+ /** Variant key */
183
+ variant: string;
184
+ /** Impressions */
185
+ impressions: number;
186
+ /** Conversions */
187
+ conversions: number;
188
+ /** Conversion rate */
189
+ conversionRate: number;
190
+ /** Statistical significance */
191
+ isSignificant: boolean;
192
+ }
@@ -0,0 +1,186 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import type { I18nService } from './i18n.service.js';
3
+ import type { I18nMiddlewareOptions, LocaleDetectionResult, Locale } from './types.js';
4
+
5
+ /**
6
+ * Request with i18n properties
7
+ */
8
+ export interface I18nRequest extends Request {
9
+ locale?: Locale;
10
+ t?: (key: string, options?: Record<string, unknown>) => string;
11
+ localeDetection?: LocaleDetectionResult;
12
+ }
13
+
14
+ /**
15
+ * Create i18n middleware
16
+ */
17
+ export function createI18nMiddleware(
18
+ i18nService: I18nService,
19
+ options: I18nMiddlewareOptions = {}
20
+ ): (req: Request, res: Response, next: NextFunction) => void {
21
+ const {
22
+ queryParam = 'lang',
23
+ cookieName = 'locale',
24
+ headerName = 'Accept-Language',
25
+ detectFromHeader = true,
26
+ } = options;
27
+
28
+ return (req: Request, res: Response, next: NextFunction): void => {
29
+ const i18nReq = req as I18nRequest;
30
+
31
+ // Detect locale
32
+ const detection = detectLocale(req, i18nService, {
33
+ queryParam,
34
+ cookieName,
35
+ headerName,
36
+ detectFromHeader,
37
+ });
38
+
39
+ i18nReq.locale = detection.locale;
40
+ i18nReq.localeDetection = detection;
41
+
42
+ // Add translation function to request
43
+ i18nReq.t = (key: string, options?: Record<string, unknown>) => {
44
+ return i18nService.t(key, {
45
+ ...options,
46
+ locale: i18nReq.locale,
47
+ });
48
+ };
49
+
50
+ // Set Content-Language header
51
+ res.setHeader('Content-Language', detection.locale);
52
+
53
+ next();
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Detect locale from request
59
+ */
60
+ export function detectLocale(
61
+ req: Request,
62
+ i18nService: I18nService,
63
+ options: I18nMiddlewareOptions = {}
64
+ ): LocaleDetectionResult {
65
+ const {
66
+ queryParam = 'lang',
67
+ cookieName = 'locale',
68
+ headerName = 'Accept-Language',
69
+ detectFromHeader = true,
70
+ } = options;
71
+
72
+ // 1. Check query parameter
73
+ if (queryParam && req.query[queryParam]) {
74
+ const locale = req.query[queryParam] as string;
75
+ if (i18nService.isLocaleSupported(locale)) {
76
+ return {
77
+ locale,
78
+ source: 'query',
79
+ confidence: 1.0,
80
+ };
81
+ }
82
+ }
83
+
84
+ // 2. Check cookie
85
+ if (cookieName && req.cookies && req.cookies[cookieName]) {
86
+ const locale = req.cookies[cookieName] as string;
87
+ if (i18nService.isLocaleSupported(locale)) {
88
+ return {
89
+ locale,
90
+ source: 'cookie',
91
+ confidence: 0.9,
92
+ };
93
+ }
94
+ }
95
+
96
+ // 3. Check Accept-Language header
97
+ if (detectFromHeader && headerName) {
98
+ const acceptLanguage = req.headers[headerName.toLowerCase()] as string | undefined;
99
+ if (acceptLanguage) {
100
+ const detectedLocale = parseAcceptLanguage(acceptLanguage, i18nService);
101
+ if (detectedLocale) {
102
+ return {
103
+ locale: detectedLocale.locale,
104
+ source: 'header',
105
+ confidence: detectedLocale.quality,
106
+ };
107
+ }
108
+ }
109
+ }
110
+
111
+ // 4. Use default locale
112
+ const defaultLocale = i18nService.getSupportedLocales()[0] || 'en';
113
+ return {
114
+ locale: defaultLocale,
115
+ source: 'default',
116
+ confidence: 0.5,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Parse Accept-Language header
122
+ */
123
+ function parseAcceptLanguage(
124
+ header: string,
125
+ i18nService: I18nService
126
+ ): { locale: Locale; quality: number } | null {
127
+ const languages = header
128
+ .split(',')
129
+ .map((lang) => {
130
+ const parts = lang.trim().split(';');
131
+ const localePart = parts[0]?.split('-')[0];
132
+ const locale = localePart || 'en'; // Get base language (e.g., 'en' from 'en-US')
133
+ const qMatch = parts[1]?.match(/q=([\d.]+)/);
134
+ const quality = qMatch?.[1] ? parseFloat(qMatch[1]) : 1.0;
135
+
136
+ return { locale, quality };
137
+ })
138
+ .sort((a, b) => b.quality - a.quality);
139
+
140
+ // Find first supported locale
141
+ for (const lang of languages) {
142
+ if (i18nService.isLocaleSupported(lang.locale)) {
143
+ return { locale: lang.locale, quality: lang.quality };
144
+ }
145
+ }
146
+
147
+ return null;
148
+ }
149
+
150
+ /**
151
+ * Locale switcher middleware
152
+ */
153
+ export function localeSwitcher(
154
+ i18nService: I18nService,
155
+ cookieName = 'locale'
156
+ ): (req: Request, res: Response) => void {
157
+ return (req: Request, res: Response): void => {
158
+ const { locale } = req.body as { locale?: string };
159
+
160
+ if (!locale) {
161
+ res.status(400).json({ error: 'Locale is required' });
162
+ return;
163
+ }
164
+
165
+ if (!i18nService.isLocaleSupported(locale)) {
166
+ res.status(400).json({
167
+ error: 'Unsupported locale',
168
+ supportedLocales: i18nService.getSupportedLocales(),
169
+ });
170
+ return;
171
+ }
172
+
173
+ // Set cookie
174
+ res.cookie(cookieName, locale, {
175
+ httpOnly: true,
176
+ secure: process.env.NODE_ENV === 'production',
177
+ maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
178
+ sameSite: 'lax',
179
+ });
180
+
181
+ res.json({
182
+ success: true,
183
+ locale,
184
+ });
185
+ };
186
+ }
@@ -0,0 +1,191 @@
1
+ import { Router } from 'express';
2
+ import type { Request, Response } from 'express';
3
+ import type { I18nService } from './i18n.service.js';
4
+ import { localeSwitcher } from './i18n.middleware.js';
5
+ import type { I18nRequest } from './i18n.middleware.js';
6
+
7
+ /**
8
+ * Create i18n routes
9
+ */
10
+ export function createI18nRoutes(i18nService: I18nService): Router {
11
+ const router = Router();
12
+
13
+ /**
14
+ * Get supported locales
15
+ * GET /locales
16
+ */
17
+ router.get('/locales', (_req: Request, res: Response) => {
18
+ const locales = i18nService.getSupportedLocales().map((locale) => {
19
+ const info = i18nService.getLocaleInfo(locale);
20
+ return {
21
+ code: locale,
22
+ name: info?.name || locale,
23
+ englishName: info?.englishName,
24
+ direction: info?.direction || 'ltr',
25
+ };
26
+ });
27
+
28
+ res.json({ locales });
29
+ });
30
+
31
+ /**
32
+ * Get current locale
33
+ * GET /locale
34
+ */
35
+ router.get('/locale', (req: Request, res: Response) => {
36
+ const i18nReq = req as I18nRequest;
37
+
38
+ res.json({
39
+ locale: i18nReq.locale,
40
+ detection: i18nReq.localeDetection,
41
+ info: i18nService.getLocaleInfo(i18nReq.locale || ''),
42
+ });
43
+ });
44
+
45
+ /**
46
+ * Switch locale
47
+ * POST /locale
48
+ * Body: { locale: 'en' }
49
+ */
50
+ router.post('/locale', localeSwitcher(i18nService));
51
+
52
+ /**
53
+ * Get translations for a namespace
54
+ * GET /translations/:namespace?locale=en
55
+ */
56
+ router.get('/translations/:namespace', (req: Request, res: Response) => {
57
+ const { namespace } = req.params;
58
+ const locale = (req.query.locale as string) || (req as I18nRequest).locale || 'en';
59
+
60
+ if (!i18nService.isLocaleSupported(locale)) {
61
+ res.status(400).json({ error: 'Unsupported locale' });
62
+ return;
63
+ }
64
+
65
+ const translations = i18nService.exportTranslations(locale, namespace);
66
+
67
+ if (!translations) {
68
+ res.status(404).json({ error: 'Translations not found' });
69
+ return;
70
+ }
71
+
72
+ res.json({
73
+ locale,
74
+ namespace,
75
+ translations,
76
+ });
77
+ });
78
+
79
+ /**
80
+ * Get translation metadata
81
+ * GET /translations/:namespace/metadata?locale=en
82
+ */
83
+ router.get('/translations/:namespace/metadata', async (req: Request, res: Response) => {
84
+ const { namespace } = req.params;
85
+ const locale = (req.query.locale as string) || (req as I18nRequest).locale || 'en';
86
+
87
+ if (!i18nService.isLocaleSupported(locale)) {
88
+ res.status(400).json({ error: 'Unsupported locale' });
89
+ return;
90
+ }
91
+
92
+ const metadata = await i18nService.getTranslationMetadata(locale, namespace);
93
+
94
+ res.json({
95
+ locale,
96
+ namespace,
97
+ metadata,
98
+ });
99
+ });
100
+
101
+ /**
102
+ * Get missing translations
103
+ * GET /translations/:namespace/missing?base=en&target=fr
104
+ */
105
+ router.get('/translations/:namespace/missing', (req: Request, res: Response) => {
106
+ const { namespace } = req.params;
107
+ const { base, target } = req.query;
108
+
109
+ if (!base || !target) {
110
+ res.status(400).json({ error: 'Base and target locales are required' });
111
+ return;
112
+ }
113
+
114
+ if (!i18nService.isLocaleSupported(base as string)) {
115
+ res.status(400).json({ error: 'Unsupported base locale' });
116
+ return;
117
+ }
118
+
119
+ if (!i18nService.isLocaleSupported(target as string)) {
120
+ res.status(400).json({ error: 'Unsupported target locale' });
121
+ return;
122
+ }
123
+
124
+ const missing = i18nService.getMissingTranslations(base as string, target as string, namespace);
125
+
126
+ res.json({
127
+ base,
128
+ target,
129
+ namespace,
130
+ missingKeys: missing,
131
+ count: missing.length,
132
+ });
133
+ });
134
+
135
+ /**
136
+ * Translate a key
137
+ * POST /translate
138
+ * Body: { key: 'welcome.message', locale: 'en', variables: { name: 'John' } }
139
+ */
140
+ router.post('/translate', (req: Request, res: Response) => {
141
+ const { key, locale, variables, namespace, defaultValue, count } = req.body as {
142
+ key: string;
143
+ locale?: string;
144
+ variables?: Record<string, string | number>;
145
+ namespace?: string;
146
+ defaultValue?: string;
147
+ count?: number;
148
+ };
149
+
150
+ if (!key) {
151
+ res.status(400).json({ error: 'Translation key is required' });
152
+ return;
153
+ }
154
+
155
+ const targetLocale = locale || (req as I18nRequest).locale || 'en';
156
+
157
+ if (!i18nService.isLocaleSupported(targetLocale)) {
158
+ res.status(400).json({ error: 'Unsupported locale' });
159
+ return;
160
+ }
161
+
162
+ const translation = i18nService.t(key, {
163
+ locale: targetLocale,
164
+ namespace,
165
+ variables,
166
+ defaultValue,
167
+ count,
168
+ });
169
+
170
+ res.json({
171
+ key,
172
+ locale: targetLocale,
173
+ translation,
174
+ });
175
+ });
176
+
177
+ /**
178
+ * Clear translation cache
179
+ * POST /cache/clear
180
+ */
181
+ router.post('/cache/clear', (_req: Request, res: Response) => {
182
+ i18nService.clearCache();
183
+
184
+ res.json({
185
+ success: true,
186
+ message: 'Translation cache cleared',
187
+ });
188
+ });
189
+
190
+ return router;
191
+ }