-
Notifications
You must be signed in to change notification settings - Fork 889
fix: WEB-862 Localise the error as per the selected language #3407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,7 +11,7 @@ import { Injectable, inject } from '@angular/core'; | |||||||||||||||||||||||
| import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** rxjs Imports */ | ||||||||||||||||||||||||
| import { EMPTY, Observable } from 'rxjs'; | ||||||||||||||||||||||||
| import { Observable } from 'rxjs'; | ||||||||||||||||||||||||
| import { catchError } from 'rxjs/operators'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** Environment Configuration */ | ||||||||||||||||||||||||
|
|
@@ -20,13 +20,14 @@ import { environment } from '../../../environments/environment'; | |||||||||||||||||||||||
| /** Custom Services */ | ||||||||||||||||||||||||
| import { Logger } from '../logger/logger.service'; | ||||||||||||||||||||||||
| import { AlertService } from '../alert/alert.service'; | ||||||||||||||||||||||||
| import { TranslateService } from '@ngx-translate/core'; // Added import for TranslateService | ||||||||||||||||||||||||
| import { TranslateService } from '@ngx-translate/core'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** Initialize Logger */ | ||||||||||||||||||||||||
| const log = new Logger('ErrorHandlerInterceptor'); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Http Request interceptor to add a default error handler to requests. | ||||||||||||||||||||||||
| * Supports localisation of error messages using Fineract's userMessageGlobalisationCode. | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| @Injectable() | ||||||||||||||||||||||||
| export class ErrorHandlerInterceptor implements HttpInterceptor { | ||||||||||||||||||||||||
|
|
@@ -42,60 +43,98 @@ export class ErrorHandlerInterceptor implements HttpInterceptor { | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Error handler. | ||||||||||||||||||||||||
| * Uses userMessageGlobalisationCode from Fineract error response to localise error messages. | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| private handleError(response: HttpErrorResponse, request: HttpRequest<any>): Observable<HttpEvent<any>> { | ||||||||||||||||||||||||
| const status = response.status; | ||||||||||||||||||||||||
| let errorMessage = response.error.developerMessage || response.message; | ||||||||||||||||||||||||
| if (response.error.errors) { | ||||||||||||||||||||||||
| let errorMessage = response.error?.developerMessage || response.message; | ||||||||||||||||||||||||
| let globalisationCode: string | null = null; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Extract globalisation code and message from Fineract error response | ||||||||||||||||||||||||
| if (response.error?.errors) { | ||||||||||||||||||||||||
| if (response.error.errors[0]) { | ||||||||||||||||||||||||
| globalisationCode = response.error.errors[0].userMessageGlobalisationCode || null; | ||||||||||||||||||||||||
| errorMessage = response.error.errors[0].defaultUserMessage || response.error.errors[0].developerMessage; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Also check top-level userMessageGlobalisationCode | ||||||||||||||||||||||||
| if (response.error?.userMessageGlobalisationCode) { | ||||||||||||||||||||||||
| globalisationCode = response.error.userMessageGlobalisationCode; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // If we have a globalisation code, try to translate it with variable substitution | ||||||||||||||||||||||||
| if (globalisationCode) { | ||||||||||||||||||||||||
| const translated = this.translate.instant(globalisationCode, response.error?.errors?.[0] || response.error || {}); | ||||||||||||||||||||||||
| // Only use translation if the key actually exists (translate returns the key itself if not found) | ||||||||||||||||||||||||
| if (translated !== globalisationCode) { | ||||||||||||||||||||||||
| errorMessage = translated; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+67
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
# Check whether the app customizes ngx-translate parsing and how existing error keys are referenced.
rg -n -C 3 "TranslateModule\.forRoot|TranslateParser|MissingTranslationHandler|TranslateLoader" src
rg -n -C 1 "errors\.[A-Za-z0-9_.-]+" src/appRepository: openMF/web-app Length of output: 10304 🏁 Script executed: cat -n src/app/core/http/error-handler.interceptor.ts | head -140Repository: openMF/web-app Length of output: 6766 🏁 Script executed: fd -e json src/assets/translations | head -10Repository: openMF/web-app Length of output: 455 🏁 Script executed: head -100 src/assets/translations/lt-LT.jsonRepository: openMF/web-app Length of output: 11943 🏁 Script executed: rg -n "instant\('error\." src/app --type ts -A 1 | head -40Repository: openMF/web-app Length of output: 2854 🏁 Script executed: rg -n "instant\('errors\." src/app --type ts -A 1 | head -40Repository: openMF/web-app Length of output: 1099 🏁 Script executed: grep -r "error\.auth\|errors\.error\.auth" src/assets/translations/Repository: openMF/web-app Length of output: 2599 🏁 Script executed: cat src/assets/translations/lt-LT.json | head -20 && cat src/assets/translations/lt-LT.json | tail -5Repository: openMF/web-app Length of output: 1155 🏁 Script executed: python3 << 'EOF'
import json
with open('src/assets/translations/lt-LT.json', 'r', encoding='utf-8') as f:
data = json.load(f)
# Check the structure of "errors" top-level keys
errors = data.get('errors', {})
print("Keys under 'errors' object:")
for key in list(errors.keys())[:15]:
print(f" {key}")
if isinstance(errors[key], dict):
for subkey in errors[key].keys():
print(f" {subkey}")
print("\nTotal keys in errors:", len(errors))
EOFRepository: openMF/web-app Length of output: 556 🏁 Script executed: cat -n src/app/app.module.ts | sed -n '72,76p'Repository: openMF/web-app Length of output: 369 🏁 Script executed: rg -n "getTranslation|onTranslationNotFound" src/app --type ts -B 2 -A 5Repository: openMF/web-app Length of output: 40 Fix translation key paths in error-handler interceptor. The hardcoded error keys on lines 84–137 are missing the The same approach used on lines 67–72 (verifying that the translation key exists before using it) should also be applied to all hardcoded error keys, or the keys should be prefixed consistently. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const isClientImage404 = status === 404 && request.url.includes('/clients/') && request.url.includes('/images'); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (!environment.production && !isClientImage404) { | ||||||||||||||||||||||||
| log.error(`Request Error: ${errorMessage}`); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (status === 401 || (environment.oauth.enabled && status === 400)) { | ||||||||||||||||||||||||
| this.alertService.alert({ type: 'Authentication Error', message: 'Invalid User Details. Please try again!' }); | ||||||||||||||||||||||||
| } else if (status === 403 && errorMessage === 'The provided one time token is invalid') { | ||||||||||||||||||||||||
| this.alertService.alert({ type: 'Invalid Token', message: 'Invalid Token. Please try again!' }); | ||||||||||||||||||||||||
| // Check specific 403 error (invalid token) BEFORE generic 403 (higher priority) | ||||||||||||||||||||||||
| if (status === 403 && globalisationCode === 'error.token.invalid') { | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: this.translate.instant('error.token.invalid.type'), | ||||||||||||||||||||||||
| message: this.translate.instant('error.token.invalid.message') | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } else if (status === 401) { | ||||||||||||||||||||||||
| // Allow Fineract translations for 401 errors | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: this.translate.instant('error.auth.type'), | ||||||||||||||||||||||||
| message: errorMessage || this.translate.instant('error.auth.message') | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } else if (environment.oauth.enabled && status === 400) { | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: this.translate.instant('error.auth.type'), | ||||||||||||||||||||||||
| message: this.translate.instant('error.auth.message') | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } else if (status === 400) { | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: 'Bad Request', | ||||||||||||||||||||||||
| message: errorMessage || 'Invalid parameters were passed in the request!' | ||||||||||||||||||||||||
| type: this.translate.instant('error.bad.request.type'), | ||||||||||||||||||||||||
| message: errorMessage || this.translate.instant('error.bad.request.message') | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } else if (status === 403) { | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: 'Unauthorized Request', | ||||||||||||||||||||||||
| message: errorMessage || 'You are not authorized for this request!' | ||||||||||||||||||||||||
| type: this.translate.instant('error.unauthorized.type'), | ||||||||||||||||||||||||
| message: errorMessage || this.translate.instant('error.unauthorized.message') | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } else if (status === 404) { | ||||||||||||||||||||||||
| // Check if this is an image request that should be silently handled (client profile image) | ||||||||||||||||||||||||
| if (isClientImage404) { | ||||||||||||||||||||||||
| // Don't show alerts for missing client images | ||||||||||||||||||||||||
| // This is an expected condition, not an error | ||||||||||||||||||||||||
| return EMPTY; | ||||||||||||||||||||||||
| // Return observable of null for missing client images so imaging service can handle gracefully | ||||||||||||||||||||||||
| return new Observable((observer) => { | ||||||||||||||||||||||||
| observer.next(null); | ||||||||||||||||||||||||
| observer.complete(); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
Comment on lines
109
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, if the goal is to let downstream request handlers (services, components) process the 404 error via their error callbacks, the interceptor should rethrow using throwError( => response) (or throwError(response) in older RxJS) from within catchError, rather than emitting null (e.g., via of(null)) from intercept. Emitting null or any value completes the observable successfully, so subscribers receive it in the next handler and the error callback is never triggered. Rethrowing propagates the original HttpErrorResponse, triggering error callbacks downstream. Example in interceptor: return next.handle(req).pipe( catchError((error: HttpErrorResponse) => { // Optional: log or handle 404 specifically without consuming if (error.status === 404) { console.log('404 intercepted but propagating'); } return throwError( => error); }) ); This pattern is standard for error-passing interceptors, as shown in official Angular docs and RxJS error handling guides. Citations:
🏁 Script executed: #!/bin/bash
# Find and examine the error-handler.interceptor.ts file
fd "error-handler.interceptor.ts" src/Repository: openMF/web-app Length of output: 104 🏁 Script executed: #!/bin/bash
# Read the error-handler.interceptor.ts file to see the context around lines 109-114
if [ -f "src/app/core/http/error-handler.interceptor.ts" ]; then
wc -l "src/app/core/http/error-handler.interceptor.ts"
fiRepository: openMF/web-app Length of output: 108 🏁 Script executed: #!/bin/bash
# Get context around lines 109-114, including imports and broader error handling
cat -n "src/app/core/http/error-handler.interceptor.ts" | head -150Repository: openMF/web-app Length of output: 6786 Rethrow the 404 instead of emitting null to let the image service handle the error. Returning a successful observable with 🔧 Suggested fix-import { Observable } from 'rxjs';
+import { Observable, throwError } from 'rxjs';
@@
- return new Observable((observer) => {
- observer.next(null);
- observer.complete();
- });
+ return throwError(() => response);🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: this.translate.instant('error.resource.not.found'), | ||||||||||||||||||||||||
| message: errorMessage || 'Resource does not exist!' | ||||||||||||||||||||||||
| type: this.translate.instant('error.resource.not.found.type'), | ||||||||||||||||||||||||
| message: errorMessage || this.translate.instant('error.resource.not.found.message') | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } else if (status === 500) { | ||||||||||||||||||||||||
| // Allow Fineract translations for 500 errors | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: 'Internal Server Error', | ||||||||||||||||||||||||
| message: 'Internal Server Error. Please try again later.' | ||||||||||||||||||||||||
| type: this.translate.instant('error.server.internal.type'), | ||||||||||||||||||||||||
| message: errorMessage || this.translate.instant('error.server.internal.message') | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } else if (status === 501) { | ||||||||||||||||||||||||
| // Allow Fineract translations for 501 errors | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: this.translate.instant('error.resource.notImplemented.type'), | ||||||||||||||||||||||||
| message: this.translate.instant('error.resource.notImplemented.message') | ||||||||||||||||||||||||
| message: errorMessage || this.translate.instant('error.resource.notImplemented.message') | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| this.alertService.alert({ type: 'Unknown Error', message: 'Unknown Error. Please try again later.' }); | ||||||||||||||||||||||||
| this.alertService.alert({ | ||||||||||||||||||||||||
| type: this.translate.instant('error.unknown.type'), | ||||||||||||||||||||||||
| message: this.translate.instant('error.unknown.message') | ||||||||||||||||||||||||
|
Comment on lines
133
to
+136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep the resolved backend message in the catch-all branch. The 🔧 Suggested fix } else {
this.alertService.alert({
type: this.translate.instant('error.unknown.type'),
- message: this.translate.instant('error.unknown.message')
+ message: errorMessage || this.translate.instant('error.unknown.message')
});
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| throw response; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't let the top-level error overwrite the nested/user-facing one.
response.error.userMessageGlobalisationCodecurrently replaceserrors[0].userMessageGlobalisationCode, and the initial fallback starts fromdeveloperMessage. When both shapes are present, the interceptor can discard the more specific nested translation and fall back to technical English text.🔧 Suggested fix
🤖 Prompt for AI Agents