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.
- package/.claude/settings.local.json +29 -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/README.md +1070 -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,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';
|