@acontplus/ng-infrastructure 3.0.0 → 3.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.
@@ -1,14 +1,88 @@
1
1
  import { HttpContextToken, HttpResponse, HttpContext, HttpClient } from '@angular/common/http';
2
2
  import * as i0 from '@angular/core';
3
- import { inject, Injectable, signal, computed, InjectionToken } from '@angular/core';
3
+ import { inject, PLATFORM_ID, signal, computed, Injectable, InjectionToken } from '@angular/core';
4
4
  import { switchMap, throwError, of, catchError, lastValueFrom } from 'rxjs';
5
+ import { Language, detectBrowserLanguages, languageToBcp47, detectBrowserLanguage, bcp47ToLanguage, getLocalizedAppMessage, AppMessageKey, getLocalizedErrorMessage } from '@acontplus/core';
5
6
  import { NotificationService } from '@acontplus/ng-notifications';
6
7
  import { AUTH_TOKEN, ENVIRONMENT, CORE_CONFIG, DEFAULT_CONFIG as DEFAULT_CONFIG$1 } from '@acontplus/ng-config';
8
+ import { isPlatformBrowser } from '@angular/common';
7
9
  import { Router } from '@angular/router';
8
10
  import { v4 } from 'uuid';
9
11
  import { finalize, catchError as catchError$1 } from 'rxjs/operators';
10
12
  import { OverlayService } from '@acontplus/ng-components';
11
13
 
14
+ const LANGUAGE_STORAGE_KEY = 'acontplus-language';
15
+ class LanguageInfo {
16
+ platformId = inject(PLATFORM_ID);
17
+ authToken = inject(AUTH_TOKEN, { optional: true });
18
+ _language = signal(Language.English, /* @ts-ignore */
19
+ ...(ngDevMode ? [{ debugName: "_language" }] : /* istanbul ignore next */ []));
20
+ _bcp47 = signal('en', /* @ts-ignore */
21
+ ...(ngDevMode ? [{ debugName: "_bcp47" }] : /* istanbul ignore next */ []));
22
+ initialized = false;
23
+ language = computed(() => this._language(), /* @ts-ignore */
24
+ ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
25
+ bcp47Tag = computed(() => this._bcp47(), /* @ts-ignore */
26
+ ...(ngDevMode ? [{ debugName: "bcp47Tag" }] : /* istanbul ignore next */ []));
27
+ getCurrentLanguage() {
28
+ this.ensureInitialized();
29
+ return this._language();
30
+ }
31
+ getBcp47Tag() {
32
+ this.ensureInitialized();
33
+ return this._bcp47();
34
+ }
35
+ getBrowserLanguages() {
36
+ return detectBrowserLanguages();
37
+ }
38
+ setLanguage(language) {
39
+ this._language.set(language);
40
+ this._bcp47.set(languageToBcp47(language));
41
+ if (isPlatformBrowser(this.platformId)) {
42
+ sessionStorage.setItem(LANGUAGE_STORAGE_KEY, language.toString());
43
+ }
44
+ }
45
+ ensureInitialized() {
46
+ if (this.initialized)
47
+ return;
48
+ this.initialized = true;
49
+ if (!isPlatformBrowser(this.platformId)) {
50
+ this._language.set(Language.English);
51
+ this._bcp47.set('en');
52
+ return;
53
+ }
54
+ const userLocale = this.authToken?.getUserData?.()?.locale;
55
+ if (userLocale) {
56
+ this._language.set(this.resolveLanguage(userLocale));
57
+ this._bcp47.set(languageToBcp47(this._language()));
58
+ return;
59
+ }
60
+ const stored = sessionStorage.getItem(LANGUAGE_STORAGE_KEY);
61
+ if (stored) {
62
+ const parsed = Number(stored);
63
+ if (parsed > 0) {
64
+ this._language.set(parsed);
65
+ this._bcp47.set(languageToBcp47(parsed));
66
+ return;
67
+ }
68
+ }
69
+ const browserLang = detectBrowserLanguage();
70
+ this._language.set(browserLang);
71
+ this._bcp47.set(languageToBcp47(browserLang));
72
+ }
73
+ resolveLanguage(locale) {
74
+ return bcp47ToLanguage(locale);
75
+ }
76
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: LanguageInfo, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
77
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: LanguageInfo, providedIn: 'root' });
78
+ }
79
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: LanguageInfo, decorators: [{
80
+ type: Injectable,
81
+ args: [{
82
+ providedIn: 'root',
83
+ }]
84
+ }] });
85
+
12
86
  //const RETRY_COUNT = 2;
13
87
  // ---------------------------------------------------------------------------
14
88
  // HTTP Context Tokens
@@ -29,6 +103,7 @@ const SHOW_NOTIFICATIONS = new HttpContextToken(() => undefined);
29
103
  const apiInterceptor = (req, next) => {
30
104
  const toastr = inject(NotificationService);
31
105
  const tokenProvider = inject(AUTH_TOKEN, { optional: true });
106
+ const languageInfo = inject(LanguageInfo);
32
107
  // Attach Bearer token when available
33
108
  const token = tokenProvider?.getToken();
34
109
  const modifiedReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;
@@ -53,10 +128,10 @@ const apiInterceptor = (req, next) => {
53
128
  // (needed to properly throw ApiResponse errors into the RxJS stream).
54
129
  switchMap((event) => {
55
130
  if (event instanceof HttpResponse) {
56
- const standardized = standardizeApiResponse(event.body);
57
- handleToastNotifications(standardized, toastr, req);
131
+ const standardized = standardizeApiResponse(event.body, languageInfo);
132
+ handleToastNotifications(standardized, toastr, req, languageInfo);
58
133
  if (standardized.status === 'error') {
59
- handleApiResponseError(standardized, toastr, req);
134
+ handleApiResponseError(standardized, toastr, req, languageInfo);
60
135
  return throwError(() => standardized);
61
136
  }
62
137
  return of(transformResponseForConsumers(standardized, event));
@@ -65,26 +140,25 @@ const apiInterceptor = (req, next) => {
65
140
  }),
66
141
  // Handle HTTP-level errors — show notification for critical errors, always re-throw.
67
142
  catchError((error) => {
68
- return handleHttpError(error, toastr);
143
+ return handleHttpError(error, toastr, languageInfo);
69
144
  }));
70
145
  };
71
146
  // ---------------------------------------------------------------------------
72
147
  // Standardisation
73
148
  // ---------------------------------------------------------------------------
74
- function standardizeApiResponse(body) {
149
+ function standardizeApiResponse(body, languageInfo) {
75
150
  if (isValidApiResponse(body))
76
151
  return body;
77
- // Wrap any non-null/undefined body (including objects with non-standard status fields)
78
152
  if (body !== null && body !== undefined) {
79
- return wrapSuccess(body);
153
+ return wrapSuccess(body, languageInfo);
80
154
  }
81
- return wrapSuccess(undefined);
155
+ return wrapSuccess(undefined, languageInfo);
82
156
  }
83
- function wrapSuccess(data) {
157
+ function wrapSuccess(data, languageInfo) {
84
158
  return {
85
159
  status: 'success',
86
160
  code: '200',
87
- message: 'Operation completed successfully',
161
+ message: getLocalizedAppMessage(AppMessageKey.OPERATION_COMPLETED, languageInfo.getCurrentLanguage()),
88
162
  data,
89
163
  timestamp: new Date().toISOString(),
90
164
  };
@@ -101,7 +175,7 @@ function isValidApiResponse(response) {
101
175
  // ---------------------------------------------------------------------------
102
176
  // Notifications
103
177
  // ---------------------------------------------------------------------------
104
- function handleToastNotifications(response, notificationService, req) {
178
+ function handleToastNotifications(response, notificationService, req, languageInfo) {
105
179
  // SKIP_NOTIFICATION always wins — even over SHOW_NOTIFICATIONS
106
180
  if (req.context.get(SKIP_NOTIFICATION))
107
181
  return;
@@ -121,17 +195,28 @@ function handleToastNotifications(response, notificationService, req) {
121
195
  });
122
196
  // Secondary: show individual warnings only when no primary message covered them
123
197
  if (response.status === 'warning' && response.warnings?.length && response.message) {
124
- response.warnings.forEach((w) => notificationService.show({ type: 'warning', message: w.message }));
198
+ response.warnings.forEach((w) => notificationService.show({
199
+ type: 'warning',
200
+ message: getLocalizedErrorMessage(w.code, languageInfo.getCurrentLanguage(), w.message),
201
+ }));
125
202
  }
126
203
  // Secondary: show individual errors only when no primary message covered them
127
204
  if (response.status === 'error' && response.errors?.length && response.message) {
128
- response.errors.forEach((e) => notificationService.show({ type: 'error', message: e.message }));
205
+ response.errors.forEach((e) => notificationService.show({
206
+ type: 'error',
207
+ message: getLocalizedErrorMessage(e.code, languageInfo.getCurrentLanguage(), e.message),
208
+ }));
129
209
  }
130
210
  }
131
- function handleApiResponseError(response, notificationService, req) {
211
+ function handleApiResponseError(response, notificationService, req, languageInfo) {
132
212
  if (req.context.get(SKIP_NOTIFICATION))
133
213
  return;
134
- const message = response.message || response.errors?.[0]?.message || 'An unexpected error occurred';
214
+ const language = languageInfo.getCurrentLanguage();
215
+ const serverMessage = response.message || response.errors?.[0]?.message;
216
+ const errorCode = response.errors?.[0]?.code ?? '';
217
+ const message = serverMessage ||
218
+ getLocalizedErrorMessage(errorCode, language) ||
219
+ getLocalizedAppMessage(AppMessageKey.UNEXPECTED_ERROR, language);
135
220
  notificationService.error({ message, config: { duration: 5000 } });
136
221
  }
137
222
  // ---------------------------------------------------------------------------
@@ -153,11 +238,11 @@ function transformResponseForConsumers(response, originalEvent) {
153
238
  // ---------------------------------------------------------------------------
154
239
  // Error handling
155
240
  // ---------------------------------------------------------------------------
156
- function handleHttpError(error, notificationService) {
241
+ function handleHttpError(error, notificationService, languageInfo) {
157
242
  if (shouldShowCriticalErrorNotification(error.status)) {
158
243
  notificationService.error({
159
- message: getCriticalErrorMessage(error),
160
- title: getErrorTitle(error.status),
244
+ message: getCriticalErrorMessage(error, languageInfo),
245
+ title: getErrorTitle(error.status, languageInfo),
161
246
  config: { duration: 5000 },
162
247
  });
163
248
  }
@@ -166,21 +251,30 @@ function handleHttpError(error, notificationService) {
166
251
  function shouldShowCriticalErrorNotification(status) {
167
252
  return status === 0 || (status >= 500 && status < 600);
168
253
  }
169
- function getErrorTitle(status) {
254
+ function getErrorTitle(status, languageInfo) {
255
+ const lang = languageInfo.getCurrentLanguage();
170
256
  if (status === 0)
171
- return 'Connection Error';
257
+ return getLocalizedAppMessage(AppMessageKey.CONNECTION_ERROR, lang);
172
258
  if (status >= 500)
173
- return 'Server Error';
174
- return 'Error';
259
+ return getLocalizedAppMessage(AppMessageKey.SERVER_ERROR, lang);
260
+ return getLocalizedAppMessage(AppMessageKey.ERROR, lang);
175
261
  }
176
- function getCriticalErrorMessage(error) {
262
+ function getCriticalErrorMessage(error, languageInfo) {
263
+ const lang = languageInfo.getCurrentLanguage();
177
264
  if (error.status === 0) {
178
- return 'Unable to connect to the server. Please check your network connection.';
265
+ return (getLocalizedErrorMessage('NETWORK_ERROR', lang) ||
266
+ getLocalizedAppMessage(AppMessageKey.NETWORK_UNAVAILABLE, lang));
179
267
  }
180
268
  if (error.status >= 500) {
181
- return (error.error?.message ?? error.message ?? 'A server error occurred. Please try again later.');
182
- }
183
- return error.error?.message ?? error.message ?? 'An unexpected error occurred';
269
+ return (error.error?.message ??
270
+ getLocalizedErrorMessage('INTERNAL_ERROR', lang) ??
271
+ error.message ??
272
+ getLocalizedAppMessage(AppMessageKey.UNEXPECTED_ERROR, lang));
273
+ }
274
+ return (error.error?.message ??
275
+ getLocalizedErrorMessage('UNHANDLED_ERROR', lang) ??
276
+ error.message ??
277
+ getLocalizedAppMessage(AppMessageKey.UNEXPECTED_ERROR, lang));
184
278
  }
185
279
  // ---------------------------------------------------------------------------
186
280
  // Utility: decide whether to auto-show toast
@@ -364,6 +458,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
364
458
  }]
365
459
  }] });
366
460
 
461
+ /**
462
+ * Returns whether a URL already identifies an external origin.
463
+ *
464
+ * Protocol-relative URLs are included because browsers resolve `//host/path`
465
+ * against the current page protocol.
466
+ */
467
+ function isAbsoluteUrl(url) {
468
+ return /^(https?:)?\/\//i.test(url);
469
+ }
470
+ /**
471
+ * Joins an API base URL and an endpoint path without changing URL protocols.
472
+ *
473
+ * This deliberately does not use `new URL(path, baseUrl)`: this function joins
474
+ * an API prefix and endpoint, while the URL standard resolves relative
475
+ * references and can discard a base path that does not end in `/`.
476
+ */
477
+ function joinApiUrl(baseUrl, path) {
478
+ if (!path) {
479
+ return baseUrl.replace(/\/+$/, '');
480
+ }
481
+ if (isAbsoluteUrl(path)) {
482
+ return path;
483
+ }
484
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
485
+ if (!normalizedBaseUrl) {
486
+ return path;
487
+ }
488
+ return `${normalizedBaseUrl}/${path.replace(/^\/+/, '')}`;
489
+ }
490
+
367
491
  // ---------------------------------------------------------------------------
368
492
  // HTTP Context Tokens & helpers
369
493
  // ---------------------------------------------------------------------------
@@ -396,6 +520,8 @@ const DEFAULT_CONFIG = {
396
520
  skipAuthUrls: ['/login', '/register', '/refresh'],
397
521
  includeAuthToken: true,
398
522
  baseUrlInjection: true,
523
+ enableLanguageHeader: true,
524
+ languageHeader: 'Accept-Language',
399
525
  refreshTokenCallback: undefined,
400
526
  logoutCallback: undefined,
401
527
  };
@@ -410,6 +536,7 @@ const httpContextInterceptor = (req, next) => {
410
536
  const router = inject(Router);
411
537
  const tenantService = inject(TenantInfo);
412
538
  const correlationService = inject(CorrelationInfo);
539
+ const languageInfo = inject(LanguageInfo);
413
540
  const loggingService = inject(LoggingService);
414
541
  const environment = inject(ENVIRONMENT);
415
542
  const config = {
@@ -431,7 +558,7 @@ const httpContextInterceptor = (req, next) => {
431
558
  // ── URL resolution ───────────────────────────────────────────────────────
432
559
  const finalUrl = resolveFinalUrl(req.url, isCustomUrl, config.baseUrlInjection, environment.apiBaseUrl);
433
560
  // ── Header construction ──────────────────────────────────────────────────
434
- const headers = buildHeaders(config, tokenProvider, correlationService, tenantService, environment, skipAuth, contextHeaders, req);
561
+ const headers = buildHeaders(config, tokenProvider, correlationService, tenantService, languageInfo, environment, skipAuth, contextHeaders, req);
435
562
  // ── Clone request ────────────────────────────────────────────────────────
436
563
  const enhancedReq = req.clone({ url: finalUrl, setHeaders: headers });
437
564
  // ── Request logging ──────────────────────────────────────────────────────
@@ -503,9 +630,12 @@ function shouldSkipAuth(url, skipAuthUrls) {
503
630
  return skipAuthUrls.some((u) => url.includes(u));
504
631
  }
505
632
  function resolveFinalUrl(url, isCustomUrl, baseUrlInjection, apiBaseUrl) {
506
- return isCustomUrl || !baseUrlInjection ? url : `${apiBaseUrl}${url}`;
633
+ if (isCustomUrl || !baseUrlInjection || isAbsoluteUrl(url)) {
634
+ return url;
635
+ }
636
+ return joinApiUrl(apiBaseUrl, url);
507
637
  }
508
- function buildHeaders(config, tokenProvider, correlationService, tenantService, environment, skipAuth, contextHeaders, req) {
638
+ function buildHeaders(config, tokenProvider, correlationService, tenantService, languageInfo, environment, skipAuth, contextHeaders, req) {
509
639
  const correlationId = config.enableCorrelationTracking
510
640
  ? correlationService.getOrCreateCorrelationId()
511
641
  : v4();
@@ -521,6 +651,9 @@ function buildHeaders(config, tokenProvider, correlationService, tenantService,
521
651
  if (tenantId) {
522
652
  headers[config.tenantIdHeader] = tenantId;
523
653
  }
654
+ if (config.enableLanguageHeader) {
655
+ headers[config.languageHeader] = languageInfo.getBcp47Tag();
656
+ }
524
657
  if (config.clientVersion) {
525
658
  headers['Client-Version'] = config.clientVersion;
526
659
  }
@@ -700,12 +833,11 @@ const spinnerInterceptor = (req, next) => {
700
833
  class BaseHttpRepository {
701
834
  http = inject(HttpClient);
702
835
  buildUrl(path = '') {
703
- const baseUrl = (this.config.baseUrl || '/api').replaceAll(/\/+/g, '');
704
- const version = this.config.version ? `/v${this.config.version}` : '';
705
- const endpoint = path
706
- ? `${this.config.endpoint}/${path}`.replaceAll(/\/+/g, '/')
707
- : this.config.endpoint;
708
- return `${baseUrl}${version}/${endpoint.replace(/^\/+/, '')}`;
836
+ const baseUrl = this.config.baseUrl || '/api';
837
+ const version = this.config.version ? `v${this.config.version}` : '';
838
+ const versionedBaseUrl = version ? joinApiUrl(baseUrl, version) : baseUrl;
839
+ const endpoint = path ? joinApiUrl(this.config.endpoint, path) : this.config.endpoint;
840
+ return joinApiUrl(versionedBaseUrl, endpoint);
709
841
  }
710
842
  get(params, path = '') {
711
843
  return this.http.get(this.buildUrl(path), { params });
@@ -793,9 +925,10 @@ class RepositoryFactory {
793
925
  create(config) {
794
926
  const buildUrl = (path = '') => {
795
927
  const baseUrl = config.baseUrl || '/api';
796
- const version = config.version ? `/v${config.version}` : '';
797
- const endpoint = path ? `${config.endpoint}/${path}` : config.endpoint;
798
- return `${baseUrl}${version}/${endpoint}`.replaceAll(/\/+/g, '/');
928
+ const version = config.version ? `v${config.version}` : '';
929
+ const versionedBaseUrl = version ? joinApiUrl(baseUrl, version) : baseUrl;
930
+ const endpoint = path ? joinApiUrl(config.endpoint, path) : config.endpoint;
931
+ return joinApiUrl(versionedBaseUrl, endpoint);
799
932
  };
800
933
  const buildParams = (pagination) => {
801
934
  const params = {};
@@ -835,11 +968,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
835
968
  args: [{ providedIn: 'root' }]
836
969
  }] });
837
970
 
838
- function mergeUrl(baseURL, endpoint) {
839
- if (!baseURL)
840
- return endpoint;
841
- return `${baseURL.replace(/\/+$/, '')}/${endpoint.replace(/^\/+/, '')}`;
842
- }
843
971
  class AngularHttpAdapter {
844
972
  http;
845
973
  baseURL;
@@ -854,7 +982,7 @@ class AngularHttpAdapter {
854
982
  };
855
983
  }
856
984
  async request(params) {
857
- const fullUrl = mergeUrl(this.baseURL, params.url);
985
+ const fullUrl = this.baseURL ? joinApiUrl(this.baseURL, params.url) : params.url;
858
986
  const httpOptions = this.buildOptions(params.options);
859
987
  const observable = this.http.request(params.method, fullUrl, {
860
988
  body: params.data,
@@ -932,7 +1060,7 @@ class CoreConfigService {
932
1060
  const baseUrl = this.config.apiBaseUrl;
933
1061
  if (!entityName)
934
1062
  return baseUrl;
935
- return `${baseUrl}/${entityName}`.replaceAll(/\/+/g, '/');
1063
+ return joinApiUrl(baseUrl, entityName);
936
1064
  }
937
1065
  /**
938
1066
  * Check if a URL should be excluded from processing
@@ -993,6 +1121,7 @@ class BaseUseCase {
993
1121
 
994
1122
  // Only create commands if you have complex validation logic
995
1123
  class Command extends BaseUseCase {
1124
+ logger = inject(LoggingService);
996
1125
  // Simple validation - override only when needed
997
1126
  validate(_request) {
998
1127
  return []; // Return array of error messages
@@ -1010,8 +1139,7 @@ class Command extends BaseUseCase {
1010
1139
  }
1011
1140
  return this.executeInternal(request).pipe(catchError$1((error) => {
1012
1141
  // Log the error for debugging
1013
- const logger = inject(LoggingService);
1014
- logger.error('An error occurred during command execution:', error);
1142
+ this.logger.error('An error occurred during command execution:', error);
1015
1143
  // Re-throw the error so the caller can handle it
1016
1144
  return throwError(() => error);
1017
1145
  }));
@@ -1019,10 +1147,10 @@ class Command extends BaseUseCase {
1019
1147
  }
1020
1148
 
1021
1149
  class Query extends BaseUseCase {
1150
+ logger = inject(LoggingService);
1022
1151
  execute(request) {
1023
1152
  return this.executeInternal(request).pipe(catchError$1((error) => {
1024
- const logger = inject(LoggingService);
1025
- logger.error('An error occurred during query execution:', error);
1153
+ this.logger.error('An error occurred during query execution:', error);
1026
1154
  return throwError(() => error);
1027
1155
  }));
1028
1156
  }
@@ -1032,5 +1160,5 @@ class Query extends BaseUseCase {
1032
1160
  * Generated bundle index. Do not edit.
1033
1161
  */
1034
1162
 
1035
- export { ActiveRequestsTracker, AngularHttpAdapter, BaseHttpRepository, BaseUseCase, Command, CoreConfigService, CorrelationInfo, GenericRepository, HTTP_CONTEXT_CONFIG, LoggingService, Query, REPOSITORY_CONFIG, RepositoryFactory, SHOW_NOTIFICATIONS, SHOW_SPINNER, SKIP_NOTIFICATION, SearchableGenericRepository, apiInterceptor, createCoreConfig, createHttpContextConfig, customUrl, httpContextInterceptor, provideCoreConfig, provideHttpContext, skipContextHeaders, spinnerInterceptor, withCustomHeaders, withoutSpinner };
1163
+ export { ActiveRequestsTracker, AngularHttpAdapter, BaseHttpRepository, BaseUseCase, Command, CoreConfigService, CorrelationInfo, GenericRepository, HTTP_CONTEXT_CONFIG, LanguageInfo, LoggingService, Query, REPOSITORY_CONFIG, RepositoryFactory, SHOW_NOTIFICATIONS, SHOW_SPINNER, SKIP_NOTIFICATION, SearchableGenericRepository, TenantInfo, apiInterceptor, createCoreConfig, createHttpContextConfig, customUrl, httpContextInterceptor, provideCoreConfig, provideHttpContext, skipContextHeaders, spinnerInterceptor, withCustomHeaders, withoutSpinner };
1036
1164
  //# sourceMappingURL=acontplus-ng-infrastructure.mjs.map