@digitaldefiance/i18n-lib 1.3.11 → 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 +8 -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
package/README.md CHANGED
@@ -2274,6 +2274,14 @@ For issues, questions, or contributions:
2274
2274
 
2275
2275
  ## ChangeLog
2276
2276
 
2277
+ ### Version 1.3.13
2278
+
2279
+ - Migrate to es2022/nx monorepo
2280
+
2281
+ ### Version 1.3.12
2282
+
2283
+ - Update typed-handleable to plugin i18n
2284
+
2277
2285
  ### Version 1.3.11
2278
2286
 
2279
2287
  - Export i18nconfig
package/package.json CHANGED
@@ -1,38 +1,23 @@
1
1
  {
2
2
  "name": "@digitaldefiance/i18n-lib",
3
- "version": "1.3.11",
4
- "description": "Generic i18n library with enum translation support",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
3
+ "version": "1.3.13",
4
+ "description": "i18n library with enum translation support",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
7
  "scripts": {
8
- "build": "yarn tsc",
9
- "test": "yarn jest --detectOpenHandles",
10
- "test:stream": "yarn jest --detectOpenHandles --runInBand --outputStyle=stream",
11
- "lint": "eslint src/**/*.ts tests/**/*.ts",
12
- "lint:fix": "eslint src/**/*.ts tests/**/*.ts --fix",
8
+ "build": "npx nx build digitaldefiance-i18n-lib",
9
+ "test": "npx nx test digitaldefiance-i18n-lib",
10
+ "lint": "npx nx lint digitaldefiance-i18n-lib",
11
+ "lint:fix": "npx nx lint digitaldefiance-i18n-lib --fix",
13
12
  "prettier:check": "prettier --check 'src/**/*.{ts,tsx}' 'tests/**/*.{ts,tsx}'",
14
13
  "prettier:fix": "prettier --write 'src/**/*.{ts,tsx}' 'tests/**/*.{ts,tsx}'",
15
- "format": "yarn prettier:fix && yarn lint:fix",
16
- "prepublishOnly": "yarn build",
14
+ "format": "npx nx format:write --projects=digitaldefiance-i18n-lib",
15
+ "format:check": "npx nx format:check --projects=digitaldefiance-i18n-lib",
16
+ "prepublishOnly": "npx nx build digitaldefiance-i18n-lib",
17
17
  "publish:public": "npm publish --access public"
18
18
  },
19
- "devDependencies": {
20
- "@types/jest": "^29.0.0",
21
- "@typescript-eslint/eslint-plugin": "^8.31.1",
22
- "@typescript-eslint/parser": "^8.31.1",
23
- "eslint": "^9.8.0",
24
- "eslint-config-prettier": "^10.1.2",
25
- "eslint-plugin-import": "^2.32.0",
26
- "eslint-plugin-prettier": "^5.3.1",
27
- "jest": "^29.0.0",
28
- "jest-util": "^30.0.5",
29
- "prettier": "^2.6.2",
30
- "prettier-plugin-organize-imports": "^4.1.0",
31
- "ts-jest": "^29.0.0",
32
- "typescript": "^5.9.2"
33
- },
34
19
  "files": [
35
- "dist",
20
+ "src",
36
21
  "README.md"
37
22
  ],
38
23
  "keywords": [
@@ -0,0 +1,30 @@
1
+ import { CurrencyCode } from './currency-code';
2
+ import { Timezone } from './timezone';
3
+ import { LanguageContextSpace } from './types';
4
+
5
+ export interface IActiveContext<TLanguage extends string> {
6
+ /**
7
+ * The default language for the user facing application
8
+ */
9
+ language: TLanguage;
10
+ /**
11
+ * The default language for the admin interface
12
+ */
13
+ adminLanguage: TLanguage;
14
+ /**
15
+ * The default currency code for the user facing application
16
+ */
17
+ currencyCode: CurrencyCode;
18
+ /**
19
+ * The default language context for the current context
20
+ */
21
+ currentContext: LanguageContextSpace;
22
+ /**
23
+ * The default timezone for the user facing application
24
+ */
25
+ timezone: Timezone;
26
+ /**
27
+ * The default timezone for the admin interface
28
+ */
29
+ adminTimezone: Timezone;
30
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Component definition with its string keys
3
+ */
4
+ export interface ComponentDefinition<TStringKeys extends string> {
5
+ /** Unique identifier for the component */
6
+ readonly id: string;
7
+ /** Human-readable name for the component */
8
+ readonly name: string;
9
+ /** Array of all string keys this component requires */
10
+ readonly stringKeys: readonly TStringKeys[];
11
+ }
@@ -0,0 +1,13 @@
1
+ import { ComponentDefinition } from './component-definition';
2
+ import { PartialComponentLanguageStrings } from './types';
3
+
4
+ /**
5
+ * Registration payload for a component with its strings
6
+ */
7
+ export interface ComponentRegistration<
8
+ TStringKeys extends string,
9
+ TLanguages extends string,
10
+ > {
11
+ readonly component: ComponentDefinition<TStringKeys>;
12
+ readonly strings: PartialComponentLanguageStrings<TStringKeys, TLanguages>;
13
+ }
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Component registry for managing internationalization components and their string translations
3
+ */
4
+
5
+ import { ComponentDefinition } from './component-definition';
6
+ import { ComponentRegistration } from './component-registration';
7
+ import { RegistryError } from './registry-error';
8
+ import { RegistryErrorType } from './registry-error-type';
9
+ import { TranslationRequest } from './translation-request';
10
+ import { TranslationResponse } from './translation-response';
11
+ import {
12
+ ComponentLanguageStrings,
13
+ ComponentStrings,
14
+ PartialComponentLanguageStrings,
15
+ PartialComponentStrings,
16
+ } from './types';
17
+ import { isTemplate, replaceVariables } from './utils';
18
+ import { ValidationConfig } from './validation-config';
19
+ import { ValidationResult } from './validation-result';
20
+
21
+ /**
22
+ * Registry for managing components and their translations
23
+ */
24
+ export class ComponentRegistry<TLanguages extends string> {
25
+ private readonly components = new Map<string, ComponentDefinition<any>>();
26
+ private readonly componentStrings = new Map<
27
+ string,
28
+ ComponentLanguageStrings<any, TLanguages>
29
+ >();
30
+ private readonly validationConfig: ValidationConfig;
31
+ private readonly registeredLanguages: Set<TLanguages>;
32
+
33
+ constructor(
34
+ languages: readonly TLanguages[],
35
+ validationConfig: ValidationConfig,
36
+ ) {
37
+ this.registeredLanguages = new Set(languages);
38
+ this.validationConfig = validationConfig;
39
+ }
40
+
41
+ /**
42
+ * Update the set of registered languages (for dynamic language addition)
43
+ */
44
+ public updateRegisteredLanguages(languages: readonly TLanguages[]): void {
45
+ this.registeredLanguages.clear();
46
+ languages.forEach((lang) => this.registeredLanguages.add(lang));
47
+ }
48
+
49
+ /**
50
+ * Register a new component with its translations
51
+ */
52
+ public registerComponent<TStringKeys extends string>(
53
+ registration: ComponentRegistration<TStringKeys, TLanguages>,
54
+ ): ValidationResult {
55
+ const { component, strings } = registration;
56
+
57
+ // Check for duplicate component
58
+ if (this.components.has(component.id)) {
59
+ throw RegistryError.createSimple(
60
+ RegistryErrorType.DuplicateComponent,
61
+ `Component '${component.id}' is already registered`,
62
+ { componentId: component.id },
63
+ );
64
+ }
65
+
66
+ // Validate the registration
67
+ const validationResult = this.validateComponentRegistration(registration);
68
+
69
+ if (
70
+ !validationResult.isValid &&
71
+ !this.validationConfig.allowPartialRegistration
72
+ ) {
73
+ throw RegistryError.createSimple(
74
+ RegistryErrorType.ValidationFailed,
75
+ `Component registration validation failed: ${validationResult.errors.join(
76
+ ', ',
77
+ )}`,
78
+ {
79
+ componentId: component.id,
80
+ missingKeys: validationResult.missingKeys,
81
+ errors: validationResult.errors,
82
+ },
83
+ );
84
+ }
85
+
86
+ // Complete missing strings with fallbacks if partial registration is allowed
87
+ const completeStrings = this.completeStringsWithFallbacks(
88
+ component,
89
+ strings,
90
+ );
91
+
92
+ // Register the component
93
+ this.components.set(component.id, component);
94
+ this.componentStrings.set(component.id, completeStrings);
95
+
96
+ return validationResult;
97
+ }
98
+
99
+ /**
100
+ * Update strings for an existing component
101
+ */
102
+ public updateComponentStrings<TStringKeys extends string>(
103
+ componentId: string,
104
+ strings: PartialComponentLanguageStrings<TStringKeys, TLanguages>,
105
+ ): ValidationResult {
106
+ const component = this.components.get(componentId);
107
+ if (!component) {
108
+ throw RegistryError.createSimple(
109
+ RegistryErrorType.ComponentNotFound,
110
+ `Component with ID '${componentId}' not found`,
111
+ { componentId },
112
+ );
113
+ }
114
+
115
+ const registration: ComponentRegistration<TStringKeys, TLanguages> = {
116
+ component: component as ComponentDefinition<TStringKeys>,
117
+ strings,
118
+ };
119
+
120
+ const validationResult = this.validateComponentRegistration(registration);
121
+
122
+ if (
123
+ validationResult.isValid ||
124
+ this.validationConfig.allowPartialRegistration
125
+ ) {
126
+ const existingStrings =
127
+ this.componentStrings.get(componentId) ||
128
+ ({} as ComponentLanguageStrings<TStringKeys, TLanguages>);
129
+ const updatedStrings = this.mergeStrings(existingStrings, strings);
130
+ const completeStrings = this.completeStringsWithFallbacks(
131
+ component,
132
+ updatedStrings,
133
+ );
134
+
135
+ this.componentStrings.set(componentId, completeStrings);
136
+ }
137
+
138
+ return validationResult;
139
+ }
140
+
141
+ /**
142
+ * Get a translation for a specific component, string key, and language
143
+ */
144
+ public getTranslation<TStringKeys extends string>(
145
+ request: TranslationRequest<TStringKeys, TLanguages>,
146
+ ): TranslationResponse {
147
+ const { componentId, stringKey, language, variables } = request;
148
+
149
+ // Check if component exists
150
+ if (!this.components.has(componentId)) {
151
+ throw RegistryError.createSimple(
152
+ RegistryErrorType.ComponentNotFound,
153
+ `Component '${componentId}' not found`,
154
+ { componentId },
155
+ );
156
+ }
157
+
158
+ const componentStrings = this.componentStrings.get(componentId);
159
+ if (!componentStrings) {
160
+ throw RegistryError.createSimple(
161
+ RegistryErrorType.StringKeyNotFound,
162
+ `No strings registered for component '${componentId}'`,
163
+ { componentId },
164
+ );
165
+ }
166
+
167
+ const targetLanguage =
168
+ language || (this.validationConfig.fallbackLanguageId as TLanguages);
169
+ let actualLanguage = targetLanguage;
170
+ let wasFallback = false;
171
+
172
+ // Try to get the string in the requested language
173
+ let languageStrings = componentStrings[targetLanguage];
174
+
175
+ // If not found and different from fallback, try fallback language
176
+ if (
177
+ !languageStrings &&
178
+ targetLanguage !== this.validationConfig.fallbackLanguageId
179
+ ) {
180
+ languageStrings =
181
+ componentStrings[
182
+ this.validationConfig.fallbackLanguageId as TLanguages
183
+ ];
184
+ actualLanguage = this.validationConfig.fallbackLanguageId as TLanguages;
185
+ wasFallback = true;
186
+ }
187
+
188
+ if (!languageStrings) {
189
+ throw RegistryError.createSimple(
190
+ RegistryErrorType.LanguageNotFound,
191
+ `No strings found for language '${targetLanguage}' in component '${componentId}'`,
192
+ { componentId, language: targetLanguage },
193
+ );
194
+ }
195
+
196
+ const translation = languageStrings[stringKey];
197
+ if (!translation) {
198
+ throw RegistryError.createSimple(
199
+ RegistryErrorType.StringKeyNotFound,
200
+ `String key '${stringKey}' not found for component '${componentId}' in language '${actualLanguage}'`,
201
+ { componentId, stringKey, language: actualLanguage },
202
+ );
203
+ }
204
+
205
+ // Process variables if the string key indicates it's a template
206
+ let processedTranslation: string = translation;
207
+ if (variables && isTemplate(stringKey)) {
208
+ processedTranslation = replaceVariables(translation, variables);
209
+ }
210
+
211
+ return {
212
+ translation: processedTranslation,
213
+ actualLanguage,
214
+ wasFallback,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Get all registered components
220
+ */
221
+ public getComponents(): ReadonlyArray<ComponentDefinition<any>> {
222
+ return Array.from(this.components.values());
223
+ }
224
+
225
+ /**
226
+ * Get a specific component by ID
227
+ */
228
+ public getComponent<TStringKeys extends string>(
229
+ componentId: string,
230
+ ): ComponentDefinition<TStringKeys> | undefined {
231
+ return this.components.get(componentId) as
232
+ | ComponentDefinition<TStringKeys>
233
+ | undefined;
234
+ }
235
+
236
+ /**
237
+ * Check if a component is registered
238
+ */
239
+ public hasComponent(componentId: string): boolean {
240
+ return this.components.has(componentId);
241
+ }
242
+
243
+ /**
244
+ * Get all strings for a component in all languages
245
+ */
246
+ public getComponentStrings<TStringKeys extends string>(
247
+ componentId: string,
248
+ ): ComponentLanguageStrings<TStringKeys, TLanguages> | undefined {
249
+ return this.componentStrings.get(componentId) as
250
+ | ComponentLanguageStrings<TStringKeys, TLanguages>
251
+ | undefined;
252
+ }
253
+
254
+ /**
255
+ * Validate a component registration
256
+ */
257
+ private validateComponentRegistration<TStringKeys extends string>(
258
+ registration: ComponentRegistration<TStringKeys, TLanguages>,
259
+ ): ValidationResult {
260
+ const { component, strings } = registration;
261
+ const missingKeys: Array<{
262
+ languageId: string;
263
+ componentId: string;
264
+ stringKey: string;
265
+ }> = [];
266
+ const errors: string[] = [];
267
+
268
+ // Check if all required string keys are provided for each language
269
+ for (const languageId of this.registeredLanguages) {
270
+ const languageStrings = strings[languageId];
271
+
272
+ if (!languageStrings) {
273
+ if (this.validationConfig.requireCompleteStrings) {
274
+ errors.push(
275
+ `Missing all strings for language '${languageId}' in component '${component.id}'`,
276
+ );
277
+ }
278
+ // Add all missing keys for this language
279
+ for (const stringKey of component.stringKeys) {
280
+ missingKeys.push({
281
+ languageId,
282
+ componentId: component.id,
283
+ stringKey,
284
+ });
285
+ }
286
+ continue;
287
+ }
288
+
289
+ // Check individual string keys
290
+ for (const stringKey of component.stringKeys) {
291
+ if (!languageStrings[stringKey]) {
292
+ missingKeys.push({
293
+ languageId,
294
+ componentId: component.id,
295
+ stringKey,
296
+ });
297
+
298
+ if (this.validationConfig.requireCompleteStrings) {
299
+ errors.push(
300
+ `Missing string key '${stringKey}' for language '${languageId}' in component '${component.id}'`,
301
+ );
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ return {
308
+ isValid: missingKeys.length === 0,
309
+ missingKeys,
310
+ errors,
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Complete missing strings with fallbacks
316
+ */
317
+ private completeStringsWithFallbacks<TStringKeys extends string>(
318
+ component: ComponentDefinition<TStringKeys>,
319
+ strings: PartialComponentLanguageStrings<TStringKeys, TLanguages>,
320
+ ): ComponentLanguageStrings<TStringKeys, TLanguages> {
321
+ const result: { [L in TLanguages]: ComponentStrings<TStringKeys> } =
322
+ {} as any;
323
+ const fallbackLanguage = this.validationConfig
324
+ .fallbackLanguageId as TLanguages;
325
+ const fallbackStrings = strings[fallbackLanguage];
326
+
327
+ // Ensure all languages have all required keys
328
+ for (const languageId of this.registeredLanguages) {
329
+ const existingLanguageStrings =
330
+ strings[languageId] || ({} as PartialComponentStrings<TStringKeys>);
331
+ const languageStrings: { [K in TStringKeys]: string } = {} as any;
332
+
333
+ for (const stringKey of component.stringKeys) {
334
+ if (existingLanguageStrings[stringKey]) {
335
+ languageStrings[stringKey] = existingLanguageStrings[stringKey]!;
336
+ } else if (fallbackStrings && fallbackStrings[stringKey]) {
337
+ // Try to use fallback language
338
+ languageStrings[stringKey] = fallbackStrings[stringKey]!;
339
+ } else {
340
+ // Last resort: use a placeholder
341
+ languageStrings[stringKey] = `[${component.id}.${stringKey}]`;
342
+ }
343
+ }
344
+
345
+ result[languageId] = languageStrings;
346
+ }
347
+
348
+ return result;
349
+ }
350
+
351
+ /**
352
+ * Merge existing strings with new strings
353
+ */
354
+ private mergeStrings<TStringKeys extends string>(
355
+ existing: ComponentLanguageStrings<TStringKeys, TLanguages>,
356
+ updates: PartialComponentLanguageStrings<TStringKeys, TLanguages>,
357
+ ): PartialComponentLanguageStrings<TStringKeys, TLanguages> {
358
+ const result: { [L in TLanguages]?: PartialComponentStrings<TStringKeys> } =
359
+ {};
360
+
361
+ // Copy existing strings
362
+ for (const [languageId, languageStrings] of Object.entries(existing) as [
363
+ TLanguages,
364
+ ComponentStrings<TStringKeys>,
365
+ ][]) {
366
+ result[languageId] = { ...languageStrings };
367
+ }
368
+
369
+ // Apply updates
370
+ for (const [languageId, languageStrings] of Object.entries(updates) as [
371
+ TLanguages,
372
+ PartialComponentStrings<TStringKeys> | undefined,
373
+ ][]) {
374
+ if (languageStrings) {
375
+ result[languageId] = {
376
+ ...result[languageId],
377
+ ...languageStrings,
378
+ };
379
+ }
380
+ }
381
+
382
+ return result;
383
+ }
384
+
385
+ /**
386
+ * Clear all components and their strings (useful for testing)
387
+ */
388
+ public clearAllComponents(): void {
389
+ this.components.clear();
390
+ this.componentStrings.clear();
391
+ }
392
+ }
@@ -0,0 +1,3 @@
1
+ export enum ContextErrorType {
2
+ InvalidContext = 'InvalidContext',
3
+ }
@@ -0,0 +1,16 @@
1
+ import { ContextErrorType } from './context-error-type';
2
+
3
+ export class ContextError extends Error {
4
+ public readonly type: ContextErrorType;
5
+ public readonly contextKey?: string;
6
+
7
+ constructor(type: ContextErrorType, contextKey?: string) {
8
+ const message = contextKey
9
+ ? `Invalid context: ${contextKey}`
10
+ : 'Invalid context';
11
+ super(message);
12
+ this.name = 'ContextError';
13
+ this.type = type;
14
+ this.contextKey = contextKey;
15
+ }
16
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Context change management for i18n systems
3
+ */
4
+ export type ContextChangeListener<T = any> = (
5
+ property: string,
6
+ oldValue: T,
7
+ newValue: T,
8
+ ) => void;
9
+
10
+ /**
11
+ * Manages context changes and notifies listeners.
12
+ */
13
+ export class ContextManager<TContext extends Record<string, any>> {
14
+ protected listeners: ContextChangeListener[] = [];
15
+
16
+ /**
17
+ * Adds a listener to be notified of context changes.
18
+ * @param listener - The listener function to add
19
+ */
20
+ public addListener(listener: ContextChangeListener): void {
21
+ this.listeners.push(listener);
22
+ }
23
+
24
+ /**
25
+ * Removes a listener from the notification list.
26
+ * @param listener - The listener function to remove
27
+ */
28
+ public removeListener(listener: ContextChangeListener): void {
29
+ const index = this.listeners.indexOf(listener);
30
+ if (index > -1) {
31
+ this.listeners.splice(index, 1);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Notifies all listeners of a context change.
37
+ * @param property - The property that changed
38
+ * @param oldValue - The old value of the property
39
+ * @param newValue - The new value of the property
40
+ */
41
+ public notifyChange<K extends keyof TContext>(
42
+ property: K,
43
+ oldValue: TContext[K],
44
+ newValue: TContext[K],
45
+ ): void {
46
+ this.listeners.forEach((listener) => {
47
+ try {
48
+ listener(property as string, oldValue, newValue);
49
+ } catch (error) {
50
+ console.error('Error in context change listener:', error);
51
+ }
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Creates a proxy for the given context to automatically notify listeners on changes.
57
+ * @param context - The context object to proxy
58
+ * @returns A proxied version of the context object
59
+ */
60
+ public createProxy(context: TContext): TContext {
61
+ const manager = this;
62
+ return new Proxy(context, {
63
+ set(target, property, value) {
64
+ const oldValue = target[property as keyof TContext];
65
+ target[property as keyof TContext] = value;
66
+ manager.notifyChange(property as keyof TContext, oldValue, value);
67
+ return true;
68
+ },
69
+ });
70
+ }
71
+ }
package/src/context.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { CurrencyCode } from './currency-code';
2
+ import { I18nContext } from './i18n-context';
3
+ import { Timezone } from './timezone';
4
+ import { LanguageContextSpace } from './types';
5
+
6
+ /**
7
+ * Creates a new I18n context with default values.
8
+ * @param defaultLanguage - The default language for the context
9
+ * @param defaultContext - The default language context
10
+ * @param defaultCurrencyCode - The default currency code (defaults to USD)
11
+ * @param defaultTimezone - The default timezone (defaults to UTC)
12
+ * @param defaultAdminTimezone - The default admin timezone (defaults to UTC)
13
+ * @returns A new I18nContext instance
14
+ */
15
+ export function createContext<TLanguage extends string>(
16
+ defaultLanguage: TLanguage,
17
+ defaultContext: LanguageContextSpace = 'admin',
18
+ defaultCurrencyCode: CurrencyCode = new CurrencyCode('USD'),
19
+ defaultTimezone: Timezone = new Timezone('UTC'),
20
+ defaultAdminTimezone: Timezone = new Timezone('UTC'),
21
+ ): I18nContext<TLanguage> {
22
+ return {
23
+ language: defaultLanguage,
24
+ adminLanguage: defaultLanguage,
25
+ currencyCode: defaultCurrencyCode,
26
+ currentContext: defaultContext,
27
+ timezone: defaultTimezone,
28
+ adminTimezone: defaultAdminTimezone,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Sets the language for the given I18n context.
34
+ * @param context - The I18n context to modify
35
+ * @param language - The language to set
36
+ */
37
+ export function setLanguage<TLanguage extends string>(
38
+ context: I18nContext<TLanguage>,
39
+ language: TLanguage,
40
+ ): void {
41
+ context.language = language;
42
+ }
43
+
44
+ /**
45
+ * Sets the admin language for the given I18n context.
46
+ * @param context - The I18n context to modify
47
+ * @param language - The admin language to set
48
+ */
49
+ export function setAdminLanguage<TLanguage extends string>(
50
+ context: I18nContext<TLanguage>,
51
+ language: TLanguage,
52
+ ): void {
53
+ context.adminLanguage = language;
54
+ }
55
+
56
+ /**
57
+ * Sets the current context for the given I18n context.
58
+ * @param context - The I18n context to modify
59
+ * @param languageContext - The language context to set
60
+ */
61
+ export function setContext<TLanguage extends string>(
62
+ context: I18nContext<TLanguage>,
63
+ languageContext: LanguageContextSpace,
64
+ ): void {
65
+ context.currentContext = languageContext;
66
+ }
67
+
68
+ /**
69
+ * Sets the timezone for the given I18n context.
70
+ * @param context - The I18n context to modify
71
+ * @param timezone - The timezone to set
72
+ */
73
+ export function setTimezone<TLanguage extends string>(
74
+ context: I18nContext<TLanguage>,
75
+ timezone: Timezone,
76
+ ): void {
77
+ context.timezone = timezone;
78
+ }
79
+
80
+ /**
81
+ * Sets the admin timezone for the given I18n context.
82
+ * @param context - The I18n context to modify
83
+ * @param timezone - The admin timezone to set
84
+ */
85
+ export function setAdminTimezone<TLanguage extends string>(
86
+ context: I18nContext<TLanguage>,
87
+ timezone: Timezone,
88
+ ): void {
89
+ context.adminTimezone = timezone;
90
+ }