http-loader icon indicating copy to clipboard operation
http-loader copied to clipboard

Cached JSON file

Open penniath opened this issue 7 years ago • 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 { }

penniath avatar Sep 06 '17 11:09 penniath

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.

stetro avatar Sep 27 '17 13:09 stetro

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

bjornharvold avatar Oct 22 '17 08:10 bjornharvold

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

mathew-pigram avatar Oct 24 '17 02:10 mathew-pigram

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.

boldwade avatar Oct 25 '17 04:10 boldwade

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 avatar Oct 25 '17 08:10 mlegenhausen

@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?

boldwade avatar Oct 26 '17 14:10 boldwade

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 :wink:

mlegenhausen avatar Oct 26 '17 15:10 mlegenhausen

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

dz-rep avatar Feb 13 '18 12:02 dz-rep

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());
}

errolgr avatar Aug 16 '18 03:08 errolgr

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.

karlhaas avatar Feb 27 '19 08:02 karlhaas

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}`,
  );
}

evenicoulddoit avatar May 23 '19 11:05 evenicoulddoit

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",
...

vixriihi avatar Jul 23 '19 05:07 vixriihi

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,
      }
    })

roffus avatar Jun 09 '20 10:06 roffus

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?

poleindraneel avatar Jul 16 '20 13:07 poleindraneel

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?

tsafadi avatar Jul 20 '20 21:07 tsafadi

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);
}

austi10 avatar Feb 05 '21 14:02 austi10

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`));
  }
}

jyzbamboo avatar Sep 29 '21 02:09 jyzbamboo

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.

maks-humeniuk avatar Oct 22 '21 13:10 maks-humeniuk