ngx-translate/http-loader

Cached JSON file

penniath opened this issue ยท 18 comments

When new bundle is deployed to production server and new translation keys have been added, last JSON is cached.
It would be nice if, somehow, the loader could fetch the last version of the JSON file.

The way I provide the loader is like this:

export const createTranslateLoader = (http: Http) => {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
};

@NgModule({
  imports: [
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: createTranslateLoader,
        deps: [ Http ]
      }
    })
  ]
}
export class CoreModule { }

Is it maybe possible to provide a cachebusting query parameter as suffix? Something like:

export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http, '/assets/i18n/', '.json?cacheBuster=' 
      + environment.cacheBusterHash);
}

Then you only have to think about providing a caching hash on every production build. But sure, it would be nice if it is provided by the TranslateHttpLoader.

Yes, would be great to have. We are experiencing the same thing.

Yes please, another vote for this. Have just run into the same issue.

We've gotten around this by having a separate pre-build gulp task that takes all the generated translation files and prepends them with a unique guid/hash. Is a hacky solution that we're not crazy about.

If you are using webpack/angular-cli you can solve the problem by writing your own TranslateLoader.

// webpack-translate-loader.ts
import { TranslateLoader } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';

export class WebpackTranslateLoader implements TranslateLoader {
  getTranslation(lang: string): Observable<any> {
    return Observable.fromPromise(System.import(`../i18n/${lang}.json`));
  }
}

Cause System will not be available you need to add it to your custom typings.d.ts

declare var System: System;
interface System {
  import(request: string): Promise<any>;
}

Now we can import everything in our app module

@NgModule({
  bootstrap: [AppComponent],
  imports: [
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useClass: WebpackTranslateLoader
      }
    })
  ]
})
export class AppModule { }

When you are using angular-cli you can then build your cache busted files via ng build --prod.

@mlegenhausen thanks for your reply!
We do something similar, but are referencing our already created translation files created with a unique hash (from our pre gulp build task):

export function createTranslateLoader(http: HttpClient, configService: ConfigService) {
  return new TranslateHttpLoader(http, '/assets/translations/', `.${configService.config.translationsHash || ''}.json`);
}
TranslateModule.forRoot({
  loader: {
    provide: TranslateLoader,
    useFactory: createTranslateLoader,
    deps: [HttpClient, ConfigService]
  }
})

What exactly do you mean about build your cache busted files via ng build?

Normally webpack is able to create this unique identifiers for you. This does not work when you use for example the copy plugin from webpack. Which can lead to caching problems when you use high ttls for your static content. I also prefer the pure webpack way so I don't have to use a build tool for my build tool ๐Ÿ˜‰

I created http interceptor to set headers {'Cache-control': 'no-cache, must-revalidate'}. Add it to providers in module and it works fine.

Similar to @stetro solution above, I use a date timestamp for my caching hash. Saves me from having to manually set a cache hash for every build.

export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http, '/assets/i18n/', '.json?cb=' + new Date().getTime());
}

We are using https://github.com/manfredsteyer/ngx-build-plus and a similar approach to @errolgr solution. We want to update the timestamp only during a new deployment.

To build on @karlhaas's comment, here's something I've come up with, which allows for cache-busting on a per-build basis, rather than @errolgr's solution - which will force the client to retrieve the JSON files on a per-request basis of your project.

  1. Find a way to expand on the webpack configuration used by Angular. @karlhaas points to ngx-build-plus, but I was already using @angular-builders/custom-webpack.

  2. Use the DefinePlugin in-built webpack plugin, to define a version - in my case just the unix timestamp. Add it as a plugin to your webpack config:

const webpack = require('webpack');

// Using @angular-builder's custom-webpack
module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      VERSION: (+new Date()).toString(),
    }),
  ],
};
  1. Now you'll have access to this VERSION constant within your project files. You'll just need to use declare to make TypeScript happy. Here's how I've used it
declare const VERSION: string;

export function createTranslateLoader(http: HttpClient) {
  return new TranslateHttpLoader(
    http, './assets/i18n/', `.json?version=${VERSION}`,
  );
}

Now with angular 8 and esnext module loading this will also work. --prod build would add hashes to file names.

import { TranslateLoader } from '@ngx-translate/core';
import { Observable, from } from 'rxjs';

export class LazyTranslateLoader implements TranslateLoader {
  getTranslation(lang: string): Observable<any> {
    return from(import(`../i18n/${lang}.json`));
  }
}

in tsconfig.json you also need

...
    "module": "esnext",
...

Now with angular 8 and esnext module loading this will also work. --prod build would add hashes to file names.

import { TranslateLoader } from '@ngx-translate/core';
import { Observable, from } from 'rxjs';

export class LazyTranslateLoader implements TranslateLoader {
  getTranslation(lang: string): Observable<any> {
    return from(import(`../i18n/${lang}.json`));
  }
}

in tsconfig.json you also need

...
    "module": "esnext",
...

Hi @vixriihi ,
I've tried your solution but the i18n json files are not copied in the dist folder.
Do I need to add something in the angular.json file?

Is correct to add the loader like the code below?

TranslateModule.forRoot({
      defaultLanguage: 'en',
      loader: {
        provide: TranslateLoader,
        useClass: LazyTranslateLoader,
      }
    })

Tried @vixriihi 's approach. But for some reason it is not working. For the record, I am building for Angular Universal. Would that make any change?

Is there a way to clear the cache with a function on logout? We have an application with multiple locations but different translations on each location. For example location A may have different translations for the translation "de" than location B.
How do I clear the cache on logout or something similar such that when you swap locations the translations will be reloaded?

For someone looking to have their translation files deployed on prod with a different hash, I solved it similar to @errolgr.
The trick is to set the environment variable with the time stamp like

export const environment ={
    hash: "${new Date().toISOString().replace(/\.|:|-/g,'')}"
}

The regex above gets rid of dot(s), colon and hyphen as a result of the date object.
The translation loader looks like follows

export function HttpLoaderFactory(http: HttpClient) {
    return isDevMode() ? new TranslateHttpLoader(http) : new TranslateHttpLoader(http, '/assets/i18n/', '.json?cb=' + environment.hash);
}

If you are using webpack/angular-cli you can solve the problem by writing your own TranslateLoader.

// webpack-translate-loader.ts
import { TranslateLoader } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';

export class WebpackTranslateLoader implements TranslateLoader {
  getTranslation(lang: string): Observable<any> {
    return Observable.fromPromise(System.import(`../i18n/${lang}.json`));
  }
}

Cause System will not be available you need to add it to your custom typings.d.ts

declare var System: System;
interface System {
  import(request: string): Promise<any>;
}

Now we can import everything in our app module

@NgModule({
  bootstrap: [AppComponent],
  imports: [
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useClass: WebpackTranslateLoader
      }
    })
  ]
})
export class AppModule { }

When you are using angular-cli you can then build your cache busted files via ng build --prod.

Thank you!!!
In my case on Angular 9 (rxjs 6.5), I use 'from' instead of 'fromPromise'. Hope it would help sb.

// webpack-translate-loader.ts
import { TranslateLoader } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';
import { from } from 'rxjs';

export class WebpackTranslateLoader implements TranslateLoader {
  getTranslation(lang: string): Observable<any> {
    return from(import(`../../../assets/i18n/${lang}.json`));
  }
}

Now with angular 8 and esnext module loading this will also work. --prod build would add hashes to file names.

import { TranslateLoader } from '@ngx-translate/core';
import { Observable, from } from 'rxjs';

export class LazyTranslateLoader implements TranslateLoader {
  getTranslation(lang: string): Observable<any> {
    return from(import(`../i18n/${lang}.json`));
  }
}

in tsconfig.json you also need

...
    "module": "esnext",
...

This approach doesn't work for me.

First, translation files aren't loaded due to ERROR Error: Uncaught (in promise): Error: Cannot find module 'assets/i18n/en-GB.json'. Tested with multiple relative paths (src/assets, assets, /assets, ../assets, ../../assets etc.) and an absolute one (https://localhost:4200/assets/i18n/en-GB.json). File opens in a separate tab by tested path, but import() can't find it.

Second, Warning: Critical dependency: the request of a dependency is an expression.