Search code examples
angularionic-frameworkngx-translatengx-translate-multi-http-loader

ngx-translate-multi-http-loader : Something went wrong for the following translation file


I was using this library ngx-translate-multi-http-loader to being able to load multiple language file for my ionic 5 App.

I didn't had any errors, but the translation wasn't working.

Then I added inside the TranslateModule.forRoot({}) the defaultLanguage: 'en', and it then resulted in the following error message

Something went wrong for the following translation file: ./assets/i18n/en.json
Database not created. Must call create() first

I needed a lot of time to figure it out how to fix it and couldn't find no help on internet.


Solution

  • 2023 Fix

    This error shouldn't happen anymore with >= 9.2.0. Since the library is now using httpBackend

    The error

    This topic pointed me in the right direction. Basically, the required loader that we use to import the translations files

    export function HttpLoaderFactory(httpClient: HttpClient) {
      return new MultiTranslateHttpLoader(httpClient, [
        { prefix: './assets/i18n/', suffix: '.json' },
        { prefix: `./assets/i18n/${APP_CONFIG.appType}/`, suffix: '.json' },
      ])
    }
    

    Is directly imported into the app.module.ts @ngModule

    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient],
      },
    }),
    

    If you find this post, it surely mean you're using an HttpInterceptor in your app, that does request the data from the ionic storage (or any other services) to apply a specific logic to the request -> Let say, you want to add a token to the request.

    Let's have the following example

    
    @Injectable()
    export class AuthHttpInterceptor implements HttpInterceptor {
      constructor(private _storage: Storage) {}
    
      intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
    
        // Clone the request to add the new header.
        let authReq: any
        return from(this.getToken()).pipe(
          switchMap((token) => {
            if (!req.headers.get('Authorization'))
              authReq = req.clone({
                headers: req.headers.set('Authorization', `Bearer ${token}`),
              })
            else authReq = req.clone()
    
            //send the newly created request
            return next.handle(authReq).pipe(
              retry(1),
              catchError((err, caught) => {
    
                const error = (err && err.error && err.error.message) || err.statusText
                return throwError(error)
              }) as any,
            )
          }),
        )
      }
    
      getToken(): Observable<any> {
        const token$ = new Observable((observer) => {
          this._storage.get('token').then(async (token) => {
            if (token) {
              observer.next(token)
            } else {
              observer.next(ORGANISATION_TOKEN)
            }
          })
        })
    
        return token$
      }
    }
    

    Then, because the ngx-translate-multi-http-loader is requesting the translations file with the angular default http class, you'll be passing through this http interceptor. But the _storage isn't instantiated yet. which result in an Database not created. Must call create() first which make our request fail into the Something went wrong for the following translation file: ./assets/i18n/en.json

    The fix

    We have to ignore this interceptor for that specific request.

    For that purpose :

    1. you could do it with a httpBackend -> here an explanation. But it wont work, because you wont have instantiate your services before the http call.

    2. You have to add a specific header, that tell your HttpInterceptor to ignore the request and let it going through without further investigation. Thx JohnrSharpe

    export const InterceptorSkipHeader = 'X-Skip-Interceptor'
    
    @Injectable()
    export class UsHttpInterceptor implements HttpInterceptor {
      constructor(private _storage: Storage, private _statusService: MegaphoneStatusService) {}
    
      intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
        if (req.headers.has(InterceptorSkipHeader)) {
          const headers = req.headers.delete(InterceptorSkipHeader)
          return next.handle(req.clone({ headers }))
        }
    
       //... Intercept
    
    

    For being able to pass that header into the ngx-translate-multi-http-loader, well.. you can't. because the library do not accept it, but it isn't complicated to copy it.

    // core.ngx-loader.ts
    
    import { HttpClient, HttpHeaders } from '@angular/common/http'
    import { TranslateLoader } from '@ngx-translate/core'
    import merge from 'deepmerge'
    import { forkJoin, Observable, of } from 'rxjs'
    import { catchError, map } from 'rxjs/operators'
    import { InterceptorSkipHeader } from '../http.interceptor'
    
    export class MultiTranslateHttpLoader implements TranslateLoader {
      constructor(
        private http: HttpClient,
        private resources: {
          prefix: string
          suffix: string
        }[],
      ) {}
    
      public getTranslation(lang: string): Observable<any> {
        const headers = new HttpHeaders().set(InterceptorSkipHeader, '') // <-- Our Skip interceptor
    
        const requests = this.resources.map((resource) => {
          const path = resource.prefix + lang + resource.suffix
    
          return this.http.get(path, { headers }).pipe( // <-- We add the header into the request
            catchError((res) => {
              console.error('Something went wrong for the following translation file:', path)
              console.error(res.message)
              return of({})
            }),
          )
        })
    
        return forkJoin(requests).pipe(map((response) => merge.all(response)))
      }
    }
    

    And there you are.