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.
Files changed (217) hide show
  1. package/.claude/settings.local.json +30 -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/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,456 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { logger } from '../../core/logger.js';
4
+ import type {
5
+ I18nConfig,
6
+ Locale,
7
+ Translation,
8
+ TranslationData,
9
+ TranslationOptions,
10
+ LocaleInfo,
11
+ TranslationMetadata,
12
+ DateFormatOptions,
13
+ NumberFormatOptions,
14
+ } from './types.js';
15
+
16
+ /**
17
+ * I18n Service
18
+ * Multi-language support with translation management
19
+ */
20
+ export class I18nService {
21
+ private config: I18nConfig;
22
+ private translations = new Map<string, TranslationData>();
23
+ private localeInfos = new Map<Locale, LocaleInfo>();
24
+ private cache = new Map<string, string>();
25
+
26
+ constructor(config: I18nConfig) {
27
+ this.config = {
28
+ fallbackLocale: config.defaultLocale,
29
+ cache: true,
30
+ debug: false,
31
+ ...config,
32
+ };
33
+
34
+ this.initializeDefaultLocales();
35
+
36
+ logger.info(
37
+ {
38
+ defaultLocale: this.config.defaultLocale,
39
+ supportedLocales: this.config.supportedLocales,
40
+ },
41
+ 'I18n service initialized'
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Initialize default locale information
47
+ */
48
+ private initializeDefaultLocales(): void {
49
+ const defaultLocales: LocaleInfo[] = [
50
+ {
51
+ code: 'en',
52
+ name: 'English',
53
+ englishName: 'English',
54
+ direction: 'ltr',
55
+ dateFormat: 'MM/DD/YYYY',
56
+ timeFormat: 'hh:mm A',
57
+ currency: 'USD',
58
+ },
59
+ {
60
+ code: 'fr',
61
+ name: 'Français',
62
+ englishName: 'French',
63
+ direction: 'ltr',
64
+ dateFormat: 'DD/MM/YYYY',
65
+ timeFormat: 'HH:mm',
66
+ currency: 'EUR',
67
+ },
68
+ {
69
+ code: 'es',
70
+ name: 'Español',
71
+ englishName: 'Spanish',
72
+ direction: 'ltr',
73
+ dateFormat: 'DD/MM/YYYY',
74
+ timeFormat: 'HH:mm',
75
+ currency: 'EUR',
76
+ },
77
+ {
78
+ code: 'de',
79
+ name: 'Deutsch',
80
+ englishName: 'German',
81
+ direction: 'ltr',
82
+ dateFormat: 'DD.MM.YYYY',
83
+ timeFormat: 'HH:mm',
84
+ currency: 'EUR',
85
+ },
86
+ {
87
+ code: 'ar',
88
+ name: 'العربية',
89
+ englishName: 'Arabic',
90
+ direction: 'rtl',
91
+ dateFormat: 'DD/MM/YYYY',
92
+ timeFormat: 'HH:mm',
93
+ currency: 'SAR',
94
+ },
95
+ {
96
+ code: 'zh',
97
+ name: '中文',
98
+ englishName: 'Chinese',
99
+ direction: 'ltr',
100
+ dateFormat: 'YYYY/MM/DD',
101
+ timeFormat: 'HH:mm',
102
+ currency: 'CNY',
103
+ },
104
+ {
105
+ code: 'ja',
106
+ name: '日本語',
107
+ englishName: 'Japanese',
108
+ direction: 'ltr',
109
+ dateFormat: 'YYYY/MM/DD',
110
+ timeFormat: 'HH:mm',
111
+ currency: 'JPY',
112
+ },
113
+ ];
114
+
115
+ for (const locale of defaultLocales) {
116
+ this.localeInfos.set(locale.code, locale);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Load translations from file
122
+ */
123
+ async loadTranslations(locale: Locale, namespace = 'common'): Promise<void> {
124
+ if (!this.config.translationsDir) {
125
+ if (this.config.debug) {
126
+ logger.warn('Translations directory not configured');
127
+ }
128
+ return;
129
+ }
130
+
131
+ try {
132
+ const filePath = path.join(this.config.translationsDir, locale, `${namespace}.json`);
133
+ const content = await fs.readFile(filePath, 'utf-8');
134
+ const data = JSON.parse(content) as TranslationData;
135
+
136
+ const key = this.getTranslationKey(locale, namespace);
137
+ this.translations.set(key, data);
138
+
139
+ logger.debug({ locale, namespace }, 'Translations loaded');
140
+ } catch (error) {
141
+ logger.error({ locale, namespace, error }, 'Failed to load translations');
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Add translations programmatically
147
+ */
148
+ addTranslations(translation: Translation): void {
149
+ const key = this.getTranslationKey(translation.locale, translation.namespace);
150
+ const existing = this.translations.get(key) || {};
151
+
152
+ this.translations.set(key, {
153
+ ...existing,
154
+ ...translation.data,
155
+ });
156
+
157
+ logger.debug(
158
+ { locale: translation.locale, namespace: translation.namespace },
159
+ 'Translations added'
160
+ );
161
+ }
162
+
163
+ /**
164
+ * Translate a key
165
+ */
166
+ t(
167
+ key: string,
168
+ options: TranslationOptions & { locale?: Locale; namespace?: string } = {}
169
+ ): string {
170
+ const locale = options.locale || this.config.defaultLocale;
171
+ const namespace = options.namespace || 'common';
172
+
173
+ // Check cache
174
+ if (this.config.cache && !options.variables && !options.count) {
175
+ const cacheKey = `${locale}:${namespace}:${key}`;
176
+ const cached = this.cache.get(cacheKey);
177
+ if (cached) {
178
+ return cached;
179
+ }
180
+ }
181
+
182
+ // Get translation
183
+ let translation = this.getTranslation(locale, namespace, key);
184
+
185
+ // Try fallback locale
186
+ if (!translation && this.config.fallbackLocale && locale !== this.config.fallbackLocale) {
187
+ translation = this.getTranslation(this.config.fallbackLocale, namespace, key);
188
+ }
189
+
190
+ // Use default value or key
191
+ if (!translation) {
192
+ translation = options.defaultValue || key;
193
+
194
+ if (this.config.debug) {
195
+ logger.warn({ locale, namespace, key }, 'Translation missing');
196
+ }
197
+ }
198
+
199
+ // Handle pluralization
200
+ if (options.count !== undefined) {
201
+ translation = this.handlePluralization(translation, options.count, locale);
202
+ }
203
+
204
+ // Interpolate variables
205
+ if (options.variables) {
206
+ translation = this.interpolate(translation, options.variables);
207
+ }
208
+
209
+ // Cache result
210
+ if (this.config.cache && !options.variables && !options.count) {
211
+ const cacheKey = `${locale}:${namespace}:${key}`;
212
+ this.cache.set(cacheKey, translation);
213
+ }
214
+
215
+ return translation;
216
+ }
217
+
218
+ /**
219
+ * Get translation value
220
+ */
221
+ private getTranslation(locale: Locale, namespace: string, key: string): string | null {
222
+ const translationKey = this.getTranslationKey(locale, namespace);
223
+ const translations = this.translations.get(translationKey);
224
+
225
+ if (!translations) {
226
+ return null;
227
+ }
228
+
229
+ // Handle nested keys (e.g., "user.profile.title")
230
+ const keys = key.split('.');
231
+ let value: string | TranslationData | undefined = translations;
232
+
233
+ for (const k of keys) {
234
+ if (value && typeof value === 'object' && k in value) {
235
+ value = value[k];
236
+ } else {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ return typeof value === 'string' ? value : null;
242
+ }
243
+
244
+ /**
245
+ * Handle pluralization
246
+ */
247
+ private handlePluralization(translation: string, count: number, locale: Locale): string {
248
+ // Simple plural rules (can be extended with Intl.PluralRules)
249
+ const pluralRules = new Intl.PluralRules(locale);
250
+ const rule = pluralRules.select(count);
251
+
252
+ // Parse plural syntax: "{{count}} item{s}"
253
+ // Or object syntax with keys: zero, one, other
254
+ if (typeof translation === 'object') {
255
+ const pluralObj = translation as unknown as Record<string, string>;
256
+ return pluralObj[rule] || pluralObj['other'] || String(translation);
257
+ }
258
+
259
+ // Simple {s} syntax
260
+ if (count === 1) {
261
+ return translation.replace(/\{s\}/g, '');
262
+ } else {
263
+ return translation.replace(/\{s\}/g, 's');
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Interpolate variables
269
+ */
270
+ private interpolate(text: string, variables: Record<string, string | number>): string {
271
+ return text.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
272
+ return variables[key]?.toString() || '';
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Format date
278
+ */
279
+ formatDate(date: Date, locale: Locale, options?: DateFormatOptions): string {
280
+ try {
281
+ return new Intl.DateTimeFormat(locale, options).format(date);
282
+ } catch (error) {
283
+ logger.error({ locale, error }, 'Date formatting failed');
284
+ return date.toISOString();
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Format number
290
+ */
291
+ formatNumber(value: number, locale: Locale, options?: NumberFormatOptions): string {
292
+ try {
293
+ return new Intl.NumberFormat(locale, options).format(value);
294
+ } catch (error) {
295
+ logger.error({ locale, error }, 'Number formatting failed');
296
+ return value.toString();
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Format currency
302
+ */
303
+ formatCurrency(value: number, locale: Locale, currency?: string): string {
304
+ const localeInfo = this.localeInfos.get(locale);
305
+ const currencyCode = currency || localeInfo?.currency || 'USD';
306
+
307
+ return this.formatNumber(value, locale, {
308
+ style: 'currency',
309
+ currency: currencyCode,
310
+ });
311
+ }
312
+
313
+ /**
314
+ * Format relative time
315
+ */
316
+ formatRelativeTime(date: Date, locale: Locale): string {
317
+ try {
318
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
319
+ const now = new Date();
320
+ const diffInSeconds = Math.floor((date.getTime() - now.getTime()) / 1000);
321
+
322
+ if (Math.abs(diffInSeconds) < 60) {
323
+ return rtf.format(diffInSeconds, 'second');
324
+ } else if (Math.abs(diffInSeconds) < 3600) {
325
+ return rtf.format(Math.floor(diffInSeconds / 60), 'minute');
326
+ } else if (Math.abs(diffInSeconds) < 86400) {
327
+ return rtf.format(Math.floor(diffInSeconds / 3600), 'hour');
328
+ } else if (Math.abs(diffInSeconds) < 2592000) {
329
+ return rtf.format(Math.floor(diffInSeconds / 86400), 'day');
330
+ } else if (Math.abs(diffInSeconds) < 31536000) {
331
+ return rtf.format(Math.floor(diffInSeconds / 2592000), 'month');
332
+ } else {
333
+ return rtf.format(Math.floor(diffInSeconds / 31536000), 'year');
334
+ }
335
+ } catch (error) {
336
+ logger.error({ locale, error }, 'Relative time formatting failed');
337
+ return date.toLocaleDateString(locale);
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Get locale info
343
+ */
344
+ getLocaleInfo(locale: Locale): LocaleInfo | undefined {
345
+ return this.localeInfos.get(locale);
346
+ }
347
+
348
+ /**
349
+ * Add or update locale info
350
+ */
351
+ setLocaleInfo(info: LocaleInfo): void {
352
+ this.localeInfos.set(info.code, info);
353
+ }
354
+
355
+ /**
356
+ * Get supported locales
357
+ */
358
+ getSupportedLocales(): Locale[] {
359
+ return this.config.supportedLocales;
360
+ }
361
+
362
+ /**
363
+ * Check if locale is supported
364
+ */
365
+ isLocaleSupported(locale: Locale): boolean {
366
+ return this.config.supportedLocales.includes(locale);
367
+ }
368
+
369
+ /**
370
+ * Get translation metadata
371
+ */
372
+ async getTranslationMetadata(locale: Locale, namespace = 'common'): Promise<TranslationMetadata> {
373
+ const key = this.getTranslationKey(locale, namespace);
374
+ const translations = this.translations.get(key);
375
+
376
+ if (!translations) {
377
+ return {
378
+ totalKeys: 0,
379
+ translatedKeys: 0,
380
+ completionPercentage: 0,
381
+ };
382
+ }
383
+
384
+ const keys = this.flattenKeys(translations);
385
+ const totalKeys = keys.length;
386
+ const translatedKeys = keys.filter((k) => translations[k]).length;
387
+
388
+ return {
389
+ totalKeys,
390
+ translatedKeys,
391
+ completionPercentage: (translatedKeys / totalKeys) * 100,
392
+ };
393
+ }
394
+
395
+ /**
396
+ * Get missing translations
397
+ */
398
+ getMissingTranslations(baseLocale: Locale, targetLocale: Locale, namespace = 'common'): string[] {
399
+ const baseKey = this.getTranslationKey(baseLocale, namespace);
400
+ const targetKey = this.getTranslationKey(targetLocale, namespace);
401
+
402
+ const baseTranslations = this.translations.get(baseKey);
403
+ const targetTranslations = this.translations.get(targetKey);
404
+
405
+ if (!baseTranslations) {
406
+ return [];
407
+ }
408
+
409
+ const baseKeys = this.flattenKeys(baseTranslations);
410
+ const targetKeys = targetTranslations ? this.flattenKeys(targetTranslations) : [];
411
+
412
+ return baseKeys.filter((key) => !targetKeys.includes(key));
413
+ }
414
+
415
+ /**
416
+ * Clear translation cache
417
+ */
418
+ clearCache(): void {
419
+ this.cache.clear();
420
+ logger.debug('Translation cache cleared');
421
+ }
422
+
423
+ /**
424
+ * Export translations
425
+ */
426
+ exportTranslations(locale: Locale, namespace = 'common'): TranslationData | null {
427
+ const key = this.getTranslationKey(locale, namespace);
428
+ return this.translations.get(key) || null;
429
+ }
430
+
431
+ /**
432
+ * Get translation key
433
+ */
434
+ private getTranslationKey(locale: Locale, namespace: string): string {
435
+ return `${locale}:${namespace}`;
436
+ }
437
+
438
+ /**
439
+ * Flatten nested keys
440
+ */
441
+ private flattenKeys(obj: TranslationData, prefix = ''): string[] {
442
+ const keys: string[] = [];
443
+
444
+ for (const [key, value] of Object.entries(obj)) {
445
+ const fullKey = prefix ? `${prefix}.${key}` : key;
446
+
447
+ if (typeof value === 'object' && value !== null) {
448
+ keys.push(...this.flattenKeys(value as TranslationData, fullKey));
449
+ } else {
450
+ keys.push(fullKey);
451
+ }
452
+ }
453
+
454
+ return keys;
455
+ }
456
+ }
@@ -0,0 +1,18 @@
1
+ export { I18nService } from './i18n.service.js';
2
+ export { createI18nMiddleware, detectLocale, localeSwitcher } from './i18n.middleware.js';
3
+ export { createI18nRoutes } from './i18n.routes.js';
4
+ export type { I18nRequest } from './i18n.middleware.js';
5
+ export type {
6
+ I18nConfig,
7
+ Locale,
8
+ Translation,
9
+ TranslationData,
10
+ TranslationOptions,
11
+ LocaleInfo,
12
+ TranslationMetadata,
13
+ I18nMiddlewareOptions,
14
+ DateFormatOptions,
15
+ NumberFormatOptions,
16
+ LocaleDetectionResult,
17
+ PluralRules,
18
+ } from './types.js';
@@ -0,0 +1,118 @@
1
+ export type Locale = string;
2
+
3
+ export interface I18nConfig {
4
+ /** Default locale */
5
+ defaultLocale: Locale;
6
+ /** Supported locales */
7
+ supportedLocales: Locale[];
8
+ /** Fallback locale when translation is missing */
9
+ fallbackLocale?: Locale;
10
+ /** Directory for translation files */
11
+ translationsDir?: string;
12
+ /** Enable debugging */
13
+ debug?: boolean;
14
+ /** Cache translations */
15
+ cache?: boolean;
16
+ }
17
+
18
+ export interface TranslationData {
19
+ [key: string]: string | TranslationData;
20
+ }
21
+
22
+ export interface Translation {
23
+ locale: Locale;
24
+ namespace: string;
25
+ data: TranslationData;
26
+ }
27
+
28
+ export interface TranslationOptions {
29
+ /** Variables to interpolate */
30
+ variables?: Record<string, string | number>;
31
+ /** Default value if translation is missing */
32
+ defaultValue?: string;
33
+ /** Count for pluralization */
34
+ count?: number;
35
+ /** Context for contextual translations */
36
+ context?: string;
37
+ }
38
+
39
+ export interface PluralRules {
40
+ zero?: string;
41
+ one?: string;
42
+ two?: string;
43
+ few?: string;
44
+ many?: string;
45
+ other: string;
46
+ }
47
+
48
+ export interface LocaleInfo {
49
+ /** Locale code (e.g., 'en-US') */
50
+ code: Locale;
51
+ /** Native name (e.g., 'English') */
52
+ name: string;
53
+ /** English name */
54
+ englishName?: string;
55
+ /** Text direction */
56
+ direction: 'ltr' | 'rtl';
57
+ /** Date format */
58
+ dateFormat?: string;
59
+ /** Time format */
60
+ timeFormat?: string;
61
+ /** Currency code */
62
+ currency?: string;
63
+ /** Number format settings */
64
+ numberFormat?: Intl.NumberFormatOptions;
65
+ }
66
+
67
+ export interface TranslationMetadata {
68
+ /** Total number of keys */
69
+ totalKeys: number;
70
+ /** Number of translated keys */
71
+ translatedKeys: number;
72
+ /** Translation completion percentage */
73
+ completionPercentage: number;
74
+ /** Last updated timestamp */
75
+ lastUpdated?: Date;
76
+ /** List of missing keys */
77
+ missingKeys?: string[];
78
+ }
79
+
80
+ export interface I18nMiddlewareOptions {
81
+ /** Query parameter name for locale */
82
+ queryParam?: string;
83
+ /** Cookie name for locale */
84
+ cookieName?: string;
85
+ /** Header name for locale */
86
+ headerName?: string;
87
+ /** Enable locale detection from Accept-Language header */
88
+ detectFromHeader?: boolean;
89
+ }
90
+
91
+ export interface DateFormatOptions {
92
+ /** Date style */
93
+ dateStyle?: 'full' | 'long' | 'medium' | 'short';
94
+ /** Time style */
95
+ timeStyle?: 'full' | 'long' | 'medium' | 'short';
96
+ /** Timezone */
97
+ timeZone?: string;
98
+ }
99
+
100
+ export interface NumberFormatOptions {
101
+ /** Number style */
102
+ style?: 'decimal' | 'currency' | 'percent' | 'unit';
103
+ /** Currency code */
104
+ currency?: string;
105
+ /** Minimum fraction digits */
106
+ minimumFractionDigits?: number;
107
+ /** Maximum fraction digits */
108
+ maximumFractionDigits?: number;
109
+ }
110
+
111
+ export interface LocaleDetectionResult {
112
+ /** Detected locale */
113
+ locale: Locale;
114
+ /** Detection source */
115
+ source: 'query' | 'cookie' | 'header' | 'default';
116
+ /** Confidence score (0-1) */
117
+ confidence: number;
118
+ }
@@ -0,0 +1,17 @@
1
+ export { MediaProcessingService } from './media-processing.service.js';
2
+ export { createMediaProcessingRoutes } from './media-processing.routes.js';
3
+ export type {
4
+ MediaProcessingConfig,
5
+ MediaType,
6
+ ImageFormat,
7
+ VideoFormat,
8
+ AudioFormat,
9
+ ImageOperation,
10
+ ImageOperationOptions,
11
+ VideoOperation,
12
+ VideoOperationOptions,
13
+ ProcessingJob,
14
+ MediaInfo,
15
+ ThumbnailOptions,
16
+ ProcessingResult,
17
+ } from './types.js';