@digitaldefiance/i18n-lib 1.3.12 → 1.3.13

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 (148) hide show
  1. package/README.md +4 -0
  2. package/package.json +12 -27
  3. package/src/active-context.ts +30 -0
  4. package/src/component-definition.ts +11 -0
  5. package/src/component-registration.ts +13 -0
  6. package/src/component-registry.ts +392 -0
  7. package/src/context-error-type.ts +3 -0
  8. package/src/context-error.ts +16 -0
  9. package/src/context-manager.ts +71 -0
  10. package/src/context.ts +90 -0
  11. package/src/core-i18n.ts +609 -0
  12. package/src/core-string-key.ts +49 -0
  13. package/src/create-translation-adapter.ts +47 -0
  14. package/src/currency-code.ts +35 -0
  15. package/{dist/currency-format.d.ts → src/currency-format.ts} +5 -4
  16. package/src/currency.ts +52 -0
  17. package/src/default-config.ts +199 -0
  18. package/src/enum-registry.ts +138 -0
  19. package/src/global-active-context.ts +255 -0
  20. package/src/handleable.ts +79 -0
  21. package/src/i-global-active-context.ts +59 -0
  22. package/src/i-handleable-error-options.ts +6 -0
  23. package/src/i-handleable.ts +5 -0
  24. package/src/i18n-config.ts +29 -0
  25. package/{dist/i18n-context.d.ts → src/i18n-context.ts} +7 -6
  26. package/src/i18n-engine.ts +491 -0
  27. package/{dist/index.d.ts → src/index.ts} +10 -1
  28. package/{dist/language-codes.d.ts → src/language-codes.ts} +23 -11
  29. package/src/language-definition.ts +13 -0
  30. package/src/language-registry.ts +292 -0
  31. package/src/plugin-i18n-engine.ts +520 -0
  32. package/src/plugin-translatable-generic-error.ts +106 -0
  33. package/src/plugin-translatable-handleable-generic.ts +60 -0
  34. package/src/plugin-typed-handleable.ts +77 -0
  35. package/src/registry-config.ts +15 -0
  36. package/src/registry-error-type.ts +12 -0
  37. package/src/registry-error.ts +74 -0
  38. package/src/strict-types.ts +35 -0
  39. package/src/template.ts +63 -0
  40. package/src/timezone.ts +20 -0
  41. package/src/translatable.ts +15 -0
  42. package/src/translation-engine.ts +8 -0
  43. package/src/translation-request.ts +12 -0
  44. package/src/translation-response.ts +8 -0
  45. package/src/typed-error.ts +384 -0
  46. package/src/typed-handleable.ts +70 -0
  47. package/{dist/types.d.ts → src/types.ts} +75 -20
  48. package/src/unified-translator.ts +96 -0
  49. package/src/utils.ts +213 -0
  50. package/src/validation-config.ts +11 -0
  51. package/src/validation-result.ts +12 -0
  52. package/dist/active-context.d.ts +0 -29
  53. package/dist/active-context.js +0 -2
  54. package/dist/component-definition.d.ts +0 -11
  55. package/dist/component-definition.js +0 -2
  56. package/dist/component-registration.d.ts +0 -9
  57. package/dist/component-registration.js +0 -2
  58. package/dist/component-registry.d.ts +0 -68
  59. package/dist/component-registry.js +0 -245
  60. package/dist/context-error-type.d.ts +0 -3
  61. package/dist/context-error-type.js +0 -7
  62. package/dist/context-error.d.ts +0 -6
  63. package/dist/context-error.js +0 -15
  64. package/dist/context-manager.d.ts +0 -33
  65. package/dist/context-manager.js +0 -61
  66. package/dist/context.d.ts +0 -44
  67. package/dist/context.js +0 -69
  68. package/dist/core-i18n.d.ts +0 -62
  69. package/dist/core-i18n.js +0 -477
  70. package/dist/core-string-key.d.ts +0 -42
  71. package/dist/core-string-key.js +0 -50
  72. package/dist/create-translation-adapter.d.ts +0 -20
  73. package/dist/create-translation-adapter.js +0 -36
  74. package/dist/currency-code.d.ts +0 -19
  75. package/dist/currency-code.js +0 -36
  76. package/dist/currency-format.js +0 -2
  77. package/dist/currency.d.ts +0 -11
  78. package/dist/currency.js +0 -48
  79. package/dist/default-config.d.ts +0 -32
  80. package/dist/default-config.js +0 -101
  81. package/dist/enum-registry.d.ts +0 -44
  82. package/dist/enum-registry.js +0 -100
  83. package/dist/global-active-context.d.ts +0 -50
  84. package/dist/global-active-context.js +0 -177
  85. package/dist/handleable.d.ts +0 -13
  86. package/dist/handleable.js +0 -56
  87. package/dist/i-global-active-context.d.ts +0 -22
  88. package/dist/i-global-active-context.js +0 -2
  89. package/dist/i-handleable-error-options.d.ts +0 -6
  90. package/dist/i-handleable-error-options.js +0 -2
  91. package/dist/i-handleable.d.ts +0 -5
  92. package/dist/i-handleable.js +0 -2
  93. package/dist/i18n-config.d.ts +0 -20
  94. package/dist/i18n-config.js +0 -2
  95. package/dist/i18n-context.js +0 -2
  96. package/dist/i18n-engine.d.ts +0 -178
  97. package/dist/i18n-engine.js +0 -338
  98. package/dist/index.js +0 -83
  99. package/dist/language-codes.js +0 -31
  100. package/dist/language-definition.d.ts +0 -13
  101. package/dist/language-definition.js +0 -2
  102. package/dist/language-registry.d.ts +0 -113
  103. package/dist/language-registry.js +0 -216
  104. package/dist/plugin-i18n-engine.d.ts +0 -146
  105. package/dist/plugin-i18n-engine.js +0 -360
  106. package/dist/plugin-translatable-generic-error.d.ts +0 -29
  107. package/dist/plugin-translatable-generic-error.js +0 -66
  108. package/dist/plugin-translatable-handleable-generic.d.ts +0 -28
  109. package/dist/plugin-translatable-handleable-generic.js +0 -40
  110. package/dist/plugin-typed-handleable.d.ts +0 -14
  111. package/dist/plugin-typed-handleable.js +0 -45
  112. package/dist/registry-config.d.ts +0 -14
  113. package/dist/registry-config.js +0 -2
  114. package/dist/registry-error-type.d.ts +0 -12
  115. package/dist/registry-error-type.js +0 -16
  116. package/dist/registry-error.d.ts +0 -18
  117. package/dist/registry-error.js +0 -45
  118. package/dist/strict-types.d.ts +0 -18
  119. package/dist/strict-types.js +0 -17
  120. package/dist/template.d.ts +0 -12
  121. package/dist/template.js +0 -30
  122. package/dist/timezone.d.ts +0 -11
  123. package/dist/timezone.js +0 -22
  124. package/dist/translatable-generic-error.d.ts +0 -29
  125. package/dist/translatable-generic-error.js +0 -66
  126. package/dist/translatable-handleable-generic.d.ts +0 -28
  127. package/dist/translatable-handleable-generic.js +0 -40
  128. package/dist/translatable.d.ts +0 -5
  129. package/dist/translatable.js +0 -11
  130. package/dist/translation-engine.d.ts +0 -8
  131. package/dist/translation-engine.js +0 -2
  132. package/dist/translation-request.d.ts +0 -9
  133. package/dist/translation-request.js +0 -2
  134. package/dist/translation-response.d.ts +0 -8
  135. package/dist/translation-response.js +0 -2
  136. package/dist/typed-error.d.ts +0 -72
  137. package/dist/typed-error.js +0 -251
  138. package/dist/typed-handleable.d.ts +0 -14
  139. package/dist/typed-handleable.js +0 -40
  140. package/dist/types.js +0 -18
  141. package/dist/unified-translator.d.ts +0 -30
  142. package/dist/unified-translator.js +0 -68
  143. package/dist/utils.d.ts +0 -64
  144. package/dist/utils.js +0 -130
  145. package/dist/validation-config.d.ts +0 -11
  146. package/dist/validation-config.js +0 -2
  147. package/dist/validation-result.d.ts +0 -12
  148. package/dist/validation-result.js +0 -2
@@ -0,0 +1,520 @@
1
+ /**
2
+ * Plugin-based internationalization engine with component and language registration
3
+ */
4
+
5
+ import { ComponentDefinition } from './component-definition';
6
+ import { ComponentRegistration } from './component-registration';
7
+ import { ComponentRegistry } from './component-registry';
8
+ import { CurrencyCode } from './currency-code';
9
+ import { EnumTranslationRegistry } from './enum-registry';
10
+ import { GlobalActiveContext } from './global-active-context';
11
+ import { IActiveContext } from './active-context';
12
+ import { LanguageDefinition } from './language-definition';
13
+ import { LanguageRegistry } from './language-registry';
14
+ import { RegistryConfig } from './registry-config';
15
+ import { RegistryError } from './registry-error';
16
+ import { RegistryErrorType } from './registry-error-type';
17
+ import { Timezone } from './timezone';
18
+ import { TranslationRequest } from './translation-request';
19
+ import { TranslationResponse } from './translation-response';
20
+ import { EnumLanguageTranslation } from './types';
21
+
22
+ /**
23
+ * Plugin-based I18n Engine with registration capabilities
24
+ */
25
+ export class PluginI18nEngine<TLanguages extends string> {
26
+ private readonly componentRegistry: ComponentRegistry<TLanguages>;
27
+ private readonly enumRegistry: EnumTranslationRegistry<string, TLanguages>;
28
+ private readonly config: RegistryConfig<TLanguages>;
29
+ private contextKey: string;
30
+
31
+ /**
32
+ * Default template processor instance
33
+ */
34
+ public readonly t: (
35
+ str: string,
36
+ language?: TLanguages,
37
+ ...otherVars: Record<string, string | number>[]
38
+ ) => string;
39
+
40
+
41
+
42
+ /**
43
+ * Static instances for semi-singleton pattern
44
+ */
45
+ private static _instances = new Map<string, PluginI18nEngine<any>>();
46
+ private static _defaultKey: string | null = null;
47
+ protected static readonly DefaultInstanceKey = 'default';
48
+
49
+ constructor(
50
+ initialLanguages: readonly LanguageDefinition[],
51
+ config: Partial<RegistryConfig<TLanguages>> = {},
52
+ ) {
53
+ // Find default language from initialLanguages or use the first one
54
+ const defaultLang =
55
+ initialLanguages.find((l) => l.isDefault) || initialLanguages[0];
56
+ if (!defaultLang) {
57
+ throw new Error('At least one language must be provided');
58
+ }
59
+
60
+ // Set up configuration with defaults
61
+ this.config = {
62
+ defaultLanguage: defaultLang.id as TLanguages,
63
+ fallbackLanguage: defaultLang.id as TLanguages,
64
+ defaultCurrencyCode: new CurrencyCode('USD'),
65
+ timezone: new Timezone('UTC'),
66
+ adminTimezone: new Timezone('UTC'),
67
+ validation: {
68
+ requireCompleteStrings: false,
69
+ allowPartialRegistration: true,
70
+ fallbackLanguageId: defaultLang.id,
71
+ },
72
+ ...config,
73
+ };
74
+
75
+ // Register initial languages in static registry (skip if already registered)
76
+ for (const lang of initialLanguages) {
77
+ if (!LanguageRegistry.hasLanguage(lang.id)) {
78
+ LanguageRegistry.registerLanguage(lang);
79
+ }
80
+ }
81
+
82
+ // Initialize component registry (per-instance)
83
+ this.componentRegistry = new ComponentRegistry<TLanguages>(
84
+ initialLanguages.map((l) => l.id as TLanguages),
85
+ this.config.validation,
86
+ );
87
+ this.enumRegistry = new EnumTranslationRegistry<string, TLanguages>(
88
+ initialLanguages.map((l) => l.id as TLanguages),
89
+ (key: string, vars?: Record<string, any>) =>
90
+ this.safeTranslate('core', key, vars),
91
+ );
92
+
93
+ // Initialize context key for this engine instance
94
+ this.contextKey = PluginI18nEngine.DefaultInstanceKey;
95
+
96
+ // Create or get the global context for this engine
97
+ const globalContext = GlobalActiveContext.getInstance<TLanguages, IActiveContext<TLanguages>>();
98
+
99
+ // Always create/update the context for this engine
100
+ globalContext.createContext(
101
+ this.config.defaultLanguage,
102
+ this.config.defaultLanguage,
103
+ this.contextKey
104
+ );
105
+ globalContext.setCurrencyCode(this.config.defaultCurrencyCode, this.contextKey);
106
+ globalContext.setUserTimezone(this.config.timezone, this.contextKey);
107
+ globalContext.setAdminTimezone(this.config.adminTimezone, this.contextKey);
108
+
109
+ // Initialize the default template processor for component-based patterns
110
+ this.t = (str: string, language?: TLanguages, ...otherVars: Record<string, string | number>[]) => {
111
+ // Step 1: Replace component-based patterns like {{componentId.stringKey}}
112
+ let result = str.replace(/\{\{([^}]+)\}\}/g, (match, pattern) => {
113
+ const parts = pattern.split('.');
114
+ if (parts.length === 2) {
115
+ const [componentId, stringKey] = parts;
116
+ // For template strings, use the first variable object if available
117
+ const isTemplate = stringKey.toLowerCase().endsWith('template');
118
+ const vars = isTemplate && otherVars.length > 0 ? otherVars[0] : {};
119
+ return this.safeTranslate(componentId.trim(), stringKey.trim(), vars, language);
120
+ }
121
+ return match; // Return original if pattern doesn't match expected format
122
+ });
123
+
124
+ // Step 2: Replace remaining variable patterns like {varName} with merged variables
125
+ const allVars = otherVars.reduce((acc, vars) => ({ ...acc, ...vars }), {});
126
+ result = result.replace(/\{(\w+)\}/g, (match, varName) => {
127
+ return allVars[varName] !== undefined ? String(allVars[varName]) : match;
128
+ });
129
+
130
+ return result;
131
+ };
132
+
133
+ // Auto-register as default instance if none exists
134
+ if (!PluginI18nEngine._defaultKey) {
135
+ PluginI18nEngine._instances.set(
136
+ PluginI18nEngine.DefaultInstanceKey,
137
+ this,
138
+ );
139
+ PluginI18nEngine._defaultKey = PluginI18nEngine.DefaultInstanceKey;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Create a new instance with a specific key
145
+ */
146
+ public static createInstance<TLangs extends string>(
147
+ key: string,
148
+ initialLanguages: readonly LanguageDefinition[],
149
+ config?: Partial<RegistryConfig<TLangs>>,
150
+ ): PluginI18nEngine<TLangs> {
151
+ if (PluginI18nEngine._instances.has(key)) {
152
+ throw RegistryError.createSimple(
153
+ RegistryErrorType.DuplicateComponent,
154
+ `I18n instance with key '${key}' already exists`,
155
+ { key },
156
+ );
157
+ }
158
+
159
+ const instance = new PluginI18nEngine<TLangs>(initialLanguages, config);
160
+ instance.contextKey = key;
161
+
162
+ // Create context for this specific instance
163
+ const globalContext = GlobalActiveContext.getInstance();
164
+ globalContext.createContext(
165
+ instance.config.defaultLanguage,
166
+ instance.config.defaultLanguage,
167
+ key
168
+ );
169
+ globalContext.setCurrencyCode(instance.config.defaultCurrencyCode, key);
170
+ globalContext.setUserTimezone(instance.config.timezone, key);
171
+ globalContext.setAdminTimezone(instance.config.adminTimezone, key);
172
+
173
+ PluginI18nEngine._instances.set(key, instance);
174
+
175
+ if (!PluginI18nEngine._defaultKey) {
176
+ PluginI18nEngine._defaultKey = key;
177
+ }
178
+
179
+ return instance;
180
+ }
181
+
182
+ /**
183
+ * Get an existing instance by key
184
+ */
185
+ public static getInstance<TLangs extends string>(
186
+ key?: string,
187
+ ): PluginI18nEngine<TLangs> {
188
+ const instanceKey =
189
+ key ||
190
+ PluginI18nEngine._defaultKey ||
191
+ PluginI18nEngine.DefaultInstanceKey;
192
+ const instance = PluginI18nEngine._instances.get(instanceKey);
193
+
194
+ if (!instance) {
195
+ throw RegistryError.createSimple(
196
+ RegistryErrorType.ComponentNotFound,
197
+ `I18n instance with key '${instanceKey}' not found`,
198
+ { key: instanceKey },
199
+ );
200
+ }
201
+
202
+ return instance as PluginI18nEngine<TLangs>;
203
+ }
204
+
205
+ /**
206
+ * Register a new language (updates static registry)
207
+ */
208
+ public registerLanguage(language: LanguageDefinition): void {
209
+ LanguageRegistry.registerLanguage(language);
210
+
211
+ // Update component registry with new language
212
+ const newLanguages = LanguageRegistry.getLanguageIds();
213
+ this.componentRegistry.updateRegisteredLanguages(
214
+ newLanguages as TLanguages[],
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Register multiple languages (updates static registry)
220
+ */
221
+ public registerLanguages(languages: readonly LanguageDefinition[]): void {
222
+ for (const language of languages) {
223
+ this.registerLanguage(language);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Register a component with its translations
229
+ */
230
+ public registerComponent<TStringKeys extends string>(
231
+ registration: ComponentRegistration<TStringKeys, TLanguages>,
232
+ ) {
233
+ return this.componentRegistry.registerComponent(registration);
234
+ }
235
+
236
+ /**
237
+ * Update strings for an existing component
238
+ */
239
+ public updateComponentStrings<TStringKeys extends string>(
240
+ componentId: string,
241
+ strings: Parameters<
242
+ ComponentRegistry<TLanguages>['updateComponentStrings']
243
+ >[1],
244
+ ) {
245
+ return this.componentRegistry.updateComponentStrings(componentId, strings);
246
+ }
247
+
248
+ /**
249
+ * Register an enum with translations
250
+ */
251
+ public registerEnum<TEnum extends string | number>(
252
+ enumObj: Record<string, TEnum>,
253
+ translations: EnumLanguageTranslation<TEnum, TLanguages>,
254
+ enumName: string,
255
+ ): void {
256
+ this.enumRegistry.register(enumObj, translations, enumName);
257
+ }
258
+
259
+ /**
260
+ * Translate a string for a component
261
+ */
262
+ public translate<TStringKeys extends string>(
263
+ componentId: string,
264
+ stringKey: TStringKeys,
265
+ variables?: Record<string, string | number>,
266
+ language?: TLanguages,
267
+ ): string {
268
+ const request: TranslationRequest<TStringKeys, TLanguages> = {
269
+ componentId,
270
+ stringKey,
271
+ language: language || this.getCurrentLanguage(),
272
+ variables,
273
+ };
274
+
275
+ const response = this.componentRegistry.getTranslation(request);
276
+ return response.translation;
277
+ }
278
+
279
+ /**
280
+ * Safe translate that returns a placeholder if translation fails
281
+ */
282
+ public safeTranslate(
283
+ componentId: string,
284
+ stringKey: string,
285
+ variables?: Record<string, string | number>,
286
+ language?: TLanguages,
287
+ ): string {
288
+ try {
289
+ return this.translate(componentId, stringKey, variables, language);
290
+ } catch (error) {
291
+ // Return a placeholder if translation fails
292
+ return `[${componentId}.${stringKey}]`;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Translate an enum value
298
+ */
299
+ public translateEnum<TEnum extends string | number>(
300
+ enumObj: Record<string, TEnum>,
301
+ value: TEnum,
302
+ language?: TLanguages,
303
+ ): string {
304
+ return this.enumRegistry.translate(
305
+ enumObj,
306
+ value,
307
+ language || this.getCurrentLanguage(),
308
+ );
309
+ }
310
+
311
+ /**
312
+ * Get detailed translation response
313
+ */
314
+ public getTranslationDetails<TStringKeys extends string>(
315
+ componentId: string,
316
+ stringKey: TStringKeys,
317
+ variables?: Record<string, string | number>,
318
+ language?: TLanguages,
319
+ ): TranslationResponse {
320
+ const request: TranslationRequest<TStringKeys, TLanguages> = {
321
+ componentId,
322
+ stringKey,
323
+ language: language || this.getCurrentLanguage(),
324
+ variables,
325
+ };
326
+
327
+ return this.componentRegistry.getTranslation(request);
328
+ }
329
+
330
+ /**
331
+ * Get current context
332
+ */
333
+ public getContext(): IActiveContext<TLanguages> {
334
+ const globalContext = GlobalActiveContext.getInstance<TLanguages, IActiveContext<TLanguages>>();
335
+ return globalContext.getContext(this.contextKey);
336
+ }
337
+
338
+ /**
339
+ * Update context
340
+ */
341
+ public updateContext(updates: Partial<IActiveContext<TLanguages>>): void {
342
+ const globalContext = GlobalActiveContext.getInstance<TLanguages, IActiveContext<TLanguages>>();
343
+
344
+ if (updates.language) globalContext.setUserLanguage(updates.language, this.contextKey);
345
+ if (updates.adminLanguage) globalContext.setAdminLanguage(updates.adminLanguage, this.contextKey);
346
+ if (updates.currencyCode) globalContext.setCurrencyCode(updates.currencyCode, this.contextKey);
347
+ if (updates.currentContext) globalContext.setLanguageContextSpace(updates.currentContext, this.contextKey);
348
+ if (updates.timezone) globalContext.setUserTimezone(updates.timezone, this.contextKey);
349
+ if (updates.adminTimezone) globalContext.setAdminTimezone(updates.adminTimezone, this.contextKey);
350
+ }
351
+
352
+ /**
353
+ * Get current language from global context
354
+ * Respects language context space (admin vs user)
355
+ */
356
+ private getCurrentLanguage(): TLanguages {
357
+ const globalContext = GlobalActiveContext.getInstance<TLanguages, IActiveContext<TLanguages>>();
358
+ const context = globalContext.getContext(this.contextKey);
359
+ return context.currentContext === 'admin' ? context.adminLanguage : context.language;
360
+ }
361
+
362
+ /**
363
+ * Set current language
364
+ */
365
+ public setLanguage(language: TLanguages): void {
366
+ if (!LanguageRegistry.hasLanguage(language)) {
367
+ throw RegistryError.createSimple(
368
+ RegistryErrorType.LanguageNotFound,
369
+ `Language '${language}' is not registered`,
370
+ { language },
371
+ );
372
+ }
373
+ const globalContext = GlobalActiveContext.getInstance<TLanguages, IActiveContext<TLanguages>>();
374
+ globalContext.setUserLanguage(language, this.contextKey);
375
+ }
376
+
377
+ /**
378
+ * Get available languages from static registry
379
+ */
380
+ public getLanguages(): readonly LanguageDefinition[] {
381
+ return LanguageRegistry.getAllLanguages();
382
+ }
383
+
384
+ /**
385
+ * Get registered components
386
+ */
387
+ public getComponents(): ReadonlyArray<ComponentDefinition<any>> {
388
+ return this.componentRegistry.getComponents();
389
+ }
390
+
391
+ /**
392
+ * Check if a component is registered
393
+ */
394
+ public hasComponent(componentId: string): boolean {
395
+ return this.componentRegistry.hasComponent(componentId);
396
+ }
397
+
398
+ /**
399
+ * Check if a language is registered in static registry
400
+ */
401
+ public hasLanguage(language: TLanguages): boolean {
402
+ return LanguageRegistry.hasLanguage(language);
403
+ }
404
+
405
+ /**
406
+ * Get language by code from static registry
407
+ */
408
+ public getLanguageByCode(code: string): LanguageDefinition | undefined {
409
+ return LanguageRegistry.getLanguageByCode(code);
410
+ }
411
+
412
+ /**
413
+ * Get component registry for direct access
414
+ */
415
+ public getComponentRegistry(): ComponentRegistry<TLanguages> {
416
+ return this.componentRegistry;
417
+ }
418
+
419
+ /**
420
+ * Get enum registry for direct access
421
+ */
422
+ public getEnumRegistry(): EnumTranslationRegistry<string, TLanguages> {
423
+ return this.enumRegistry;
424
+ }
425
+
426
+ /**
427
+ * Validate that all components have complete translations
428
+ */
429
+ public validateAllComponents(): {
430
+ isValid: boolean;
431
+ errors: string[];
432
+ warnings: string[];
433
+ } {
434
+ const errors: string[] = [];
435
+ const warnings: string[] = [];
436
+ let isValid = true;
437
+
438
+ const components = this.getComponents();
439
+ const languages = LanguageRegistry.getLanguageIds();
440
+
441
+ for (const component of components) {
442
+ const componentStrings = this.componentRegistry.getComponentStrings(
443
+ component.id,
444
+ );
445
+
446
+ if (!componentStrings) {
447
+ errors.push(`Component '${component.id}' has no registered strings`);
448
+ isValid = false;
449
+ continue;
450
+ }
451
+
452
+ for (const language of languages) {
453
+ const languageStrings = componentStrings[language as TLanguages];
454
+
455
+ if (!languageStrings) {
456
+ errors.push(
457
+ `Component '${component.id}' missing strings for language '${language}'`,
458
+ );
459
+ isValid = false;
460
+ continue;
461
+ }
462
+
463
+ for (const stringKey of component.stringKeys) {
464
+ if (!languageStrings[stringKey]) {
465
+ warnings.push(
466
+ `Component '${component.id}' missing key '${stringKey}' for language '${language}'`,
467
+ );
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ return { isValid, errors, warnings };
474
+ }
475
+
476
+ /**
477
+ * Clear all component registrations for this instance (useful for testing)
478
+ */
479
+ public clearAllComponents(): void {
480
+ this.componentRegistry.clearAllComponents();
481
+ }
482
+
483
+ /**
484
+ * Remove a specific named instance
485
+ */
486
+ public static removeInstance(key?: string): boolean {
487
+ const instanceKey = key || PluginI18nEngine.DefaultInstanceKey;
488
+ const removed = PluginI18nEngine._instances.delete(instanceKey);
489
+
490
+ // If we removed the default instance, clear the default key
491
+ if (removed && PluginI18nEngine._defaultKey === instanceKey) {
492
+ PluginI18nEngine._defaultKey = null;
493
+ }
494
+
495
+ return removed;
496
+ }
497
+
498
+ /**
499
+ * Check if an instance exists
500
+ */
501
+ public static hasInstance(key?: string): boolean {
502
+ const instanceKey = key || PluginI18nEngine.DefaultInstanceKey;
503
+ return PluginI18nEngine._instances.has(instanceKey);
504
+ }
505
+
506
+ /**
507
+ * Reset all plugin engines and clear component registrations
508
+ * Useful for test cleanup
509
+ */
510
+ public static resetAll(): void {
511
+ // Clear component registrations for each engine BEFORE clearing instances
512
+ for (const engine of PluginI18nEngine._instances.values()) {
513
+ engine.clearAllComponents();
514
+ }
515
+ PluginI18nEngine._instances.clear();
516
+ PluginI18nEngine._defaultKey = null;
517
+ LanguageRegistry.clear();
518
+ GlobalActiveContext.clearAll();
519
+ }
520
+ }
@@ -0,0 +1,106 @@
1
+ import { PluginI18nEngine } from './plugin-i18n-engine';
2
+
3
+ /**
4
+ * Generic translatable error that works with any plugin engine and component
5
+ */
6
+ export class PluginTranslatableGenericError<
7
+ TStringKey extends string = string,
8
+ TLanguage extends string = string,
9
+ > extends Error {
10
+ public readonly stringKey: TStringKey;
11
+ public readonly componentId: string;
12
+ public readonly language?: TLanguage;
13
+ public readonly variables?: Record<string, string | number>;
14
+ public readonly metadata?: Record<string, any>;
15
+
16
+ /**
17
+ * Create a translatable error
18
+ * @param componentId - The component ID to translate from
19
+ * @param stringKey - The translation key
20
+ * @param variables - Variables for interpolation
21
+ * @param language - Optional language override
22
+ * @param metadata - Additional error metadata
23
+ * @param instanceKey - Optional engine instance key
24
+ */
25
+ constructor(
26
+ componentId: string,
27
+ stringKey: TStringKey,
28
+ variables?: Record<string, string | number>,
29
+ language?: TLanguage,
30
+ metadata?: Record<string, any>,
31
+ instanceKey?: string,
32
+ ) {
33
+ let translatedMessage: string;
34
+
35
+ try {
36
+ const engine = PluginI18nEngine.getInstance<TLanguage>(instanceKey);
37
+ translatedMessage = engine.safeTranslate(
38
+ componentId,
39
+ stringKey,
40
+ variables,
41
+ language,
42
+ );
43
+ } catch (error) {
44
+ // If engine not found or translation fails, use fallback format
45
+ translatedMessage = `[${componentId}.${stringKey}]`;
46
+ }
47
+
48
+ super(translatedMessage);
49
+ this.name = 'TranslatableGenericError';
50
+ this.stringKey = stringKey;
51
+ this.componentId = componentId;
52
+ this.language = language;
53
+ this.variables = variables;
54
+ this.metadata = metadata;
55
+
56
+ // Ensure proper prototype chain for instanceof checks across transpiled targets
57
+ }
58
+
59
+ /**
60
+ * Create error with explicit engine instance
61
+ */
62
+ static withEngine<TStringKey extends string, TLanguage extends string>(
63
+ engine: PluginI18nEngine<TLanguage>,
64
+ componentId: string,
65
+ stringKey: TStringKey,
66
+ variables?: Record<string, string | number>,
67
+ language?: TLanguage,
68
+ metadata?: Record<string, any>,
69
+ ): PluginTranslatableGenericError<TStringKey, TLanguage> {
70
+ const translatedMessage = engine.safeTranslate(
71
+ componentId,
72
+ stringKey,
73
+ variables,
74
+ language,
75
+ );
76
+
77
+ const error = Object.create(PluginTranslatableGenericError.prototype);
78
+ Error.call(error, translatedMessage);
79
+ error.name = 'TranslatableGenericError';
80
+ error.stringKey = stringKey;
81
+ error.componentId = componentId;
82
+ error.language = language;
83
+ error.variables = variables;
84
+ error.metadata = metadata;
85
+ error.message = translatedMessage;
86
+
87
+ return error;
88
+ }
89
+
90
+ /**
91
+ * Retranslate the error message in a different language
92
+ */
93
+ retranslate(language: TLanguage, instanceKey?: string): string {
94
+ try {
95
+ const engine = PluginI18nEngine.getInstance<TLanguage>(instanceKey);
96
+ return engine.safeTranslate(
97
+ this.componentId,
98
+ this.stringKey,
99
+ this.variables,
100
+ language,
101
+ );
102
+ } catch (error) {
103
+ return `[${this.componentId}.${this.stringKey}]`;
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,60 @@
1
+ import { IHandleable } from './i-handleable';
2
+ import { PluginTranslatableGenericError } from './plugin-translatable-generic-error';
3
+
4
+ /**
5
+ * Generic translatable error that works with any plugin engine and component
6
+ */
7
+ export class PluginTranslatableHandleableGenericError<
8
+ TStringKey extends string = string,
9
+ TLanguage extends string = string,
10
+ >
11
+ extends PluginTranslatableGenericError<TStringKey, TLanguage>
12
+ implements IHandleable
13
+ {
14
+ private _handled = false;
15
+ public override readonly cause?: Error;
16
+ public readonly statusCode: number;
17
+ public readonly sourceData?: unknown;
18
+
19
+ /**
20
+ * Create a translatable error
21
+ * @param componentId - The component ID to translate from
22
+ * @param stringKey - The translation key
23
+ * @param variables - Variables for interpolation
24
+ * @param language - Optional language override
25
+ * @param metadata - Additional error metadata
26
+ * @param instanceKey - Optional engine instance key
27
+ */
28
+ constructor(
29
+ componentId: string,
30
+ stringKey: TStringKey,
31
+ variables?: Record<string, string | number>,
32
+ language?: TLanguage,
33
+ metadata?: Record<string, any>,
34
+ instanceKey?: string,
35
+ handleableOptions?: {
36
+ statusCode?: number;
37
+ cause?: Error;
38
+ sourceData?: unknown;
39
+ },
40
+ ) {
41
+ super(componentId, stringKey, variables, language, metadata, instanceKey);
42
+ this.statusCode = handleableOptions?.statusCode ?? 500;
43
+ this.cause = handleableOptions?.cause;
44
+ this.sourceData = handleableOptions?.sourceData;
45
+ }
46
+ public get handled(): boolean {
47
+ return this._handled;
48
+ }
49
+ public set handled(value: boolean) {
50
+ this._handled = value;
51
+ }
52
+ toJSON(): Record<string, unknown> {
53
+ return {
54
+ statusCode: this.statusCode,
55
+ message: this.message,
56
+ cause: this.cause,
57
+ sourceData: this.sourceData,
58
+ };
59
+ }
60
+ }