String Resources in Angular

AndreaM16's answer is definitely thorough, but you sould also consider using an existing package for this instead of writing your own custom code that you need to maintain. To that end, I would recommend checking out ngx-translate (which is used somewhat under the hood in that answer), transloco and Angular's built-in i18n capabilities.

Most of those approaches are based on the magic strings, where your HTML templates and typescript code need to contain strings that (hopefully) match the keys in a JSON or XML file. This presents a serious code maintenance issue which won't be revealed at compile time, only at runtime. I would also advocate for checking out some guides on creating a type-safe translation system:

  • https://medium.com/angular-in-depth/angular-typed-translations-29353f0a60bc
  • https://medium.com/angular-in-depth/dynamic-import-of-locales-in-angular-b994d3c07197

Having configuration, translations and resources separated from application's logic is very useful. Configuration would also be very helpful in other context like, for example, getting api_url useful for any rest call.

You can set up such thing using @angular/cli. Having the following application structure:

|- app
|- assets
         |- i18n
                - en.json
                - it.json
         |- json-config
                - development.json
                - env.json
                - production.json
         |- resources
                - en.json
                - it.json
|- environment
         - environment.prod.ts
         - environment.ts

|- config
         - app.config.ts    

Where:

  • app: contain all application logics
  • assets/i18n/*.json: contains a textual resources that can be used in any of your components. There's one of them for each language we want to cover.

E.G. en.json:

{
  "TEST": {
    "WELCOME"  : "Welcome"
}

E.G it.json:

{
  "TEST": {
    "WELCOME"  : "Benvenuto"
}
  • assets/json-config: contains configuration files to use in development mode and production mode. Also contains env.json which is a json that says which is the current development mode:

E.G. env.json:

{
   "env" : "development"
}

E.G. development.json:

{
    "API_URL"   : "someurl",
    "MYTOKEN" : "sometoken",
    "DEBUGGING" : true
}
  • assets/resources: contains jsons files of resources per each language we want to cover. For instance, it may contain jsons initialization's for application models. It's useful if, for example, you want to fill an array of a model to be passed to an *ngFor personalized based on enviroment and/or language. Such initialization should be done inside each component which want to access a precise resource via AppConfig.getResourceByKey that will be shown later.

  • app.config.ts: Configuration Service that loads resources based on development mode. I will show a snippet below.

Basic Configuration:

In order to load basic configuration files as the application starts we need to do a few things.

app.module.ts:

import { NgModule, APP_INITIALIZER } from '@angular/core';
/** App Services **/
import { AppConfig } from '../config/app.config';
import { TranslationConfigModule } from './shared/modules/translation.config.module';

// Calling load to get configuration + translation
export function initResources(config: AppConfig, translate: TranslationConfigModule) {
        return () => config.load(translate);
}

// Initializing Resources and Translation as soon as possible
@NgModule({
     . . .
     imports: [
         . . .
         TranslationConfigModule
     ],
     providers: [
         AppConfig, {
           provide: APP_INITIALIZER,
           useFactory: initResources,
           deps: [AppConfig, TranslationConfigModule],
           multi: true
         }
     ],
     bootstrap: [AppComponent]
})
export class AppModule { }

app.config.ts:

As said above, this service loads configuration files based on development mode and, in this case, browser language. Loading resources based on language can be very useful if you want to customize your application. For example, your italian distribution would have different routes, different behavior or simple different texts.

Every Resources, Configuration and Enviroment entry is available trough AppConfig service's methods such as getEnvByKey, getEntryByKey and getResourceByKey.

import { Inject, Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { get } from 'lodash';
import 'rxjs/add/operator/catch';

import { TranslationConfigModule } from '../app/shared/modules/translation.config.module';

@Injectable()
export class AppConfig {

  private _configurations: any = new Object();
  private _config_path = './assets/json-config/';
  private _resources_path = './assets/resources/';

  constructor( private http: Http) { }

  // Get an Environment Entry by Key
  public getEnvByKey(key: any): any {
    return this._configurations.env[key];
  }

  // Get a Configuration Entryby Key
  public getEntryByKey(key: any): any {
    return this._configurations.config[key];
  }

  // Get a Resource Entry by Key
  public getResourceByKey(key: any): any {
    return get(this._configurations.resource, key);
  }

  // Should be self-explanatory 
  public load(translate: TranslationConfigModule){
    return new Promise((resolve, reject) => {
      // Given env.json
      this.loadFile(this._config_path + 'env.json').then((envData: any) => {
        this._configurations.env = envData;
        // Load production or development configuration file based on before
        this.loadFile(this._config_path + envData.env  + '.json').then((conf: any) => {
          this._configurations.config = conf;
          // Load resources files based on browser language
          this.loadFile(this._resources_path + translate.getBrowserLang() +'.json').then((resource: any) => {
            this._configurations.resource = resource;
            return resolve(true);
          });
        });
      });
    });
  }

  private loadFile(path: string){
    return new Promise((resolve, reject) => {
      this.http.get(path)
        .map(res => res.json())
        .catch((error: any) => {
          console.error(error);
          return Observable.throw(error.json().error || 'Server error');
        })
        .subscribe((res_data) => {
          return resolve(res_data);
        })
    });
  }

}

translation.config.module.ts

This module sets up translation built using ngx-translate. Sets up translation depending on the browser language.

import { HttpModule, Http } from '@angular/http';
import { NgModule, ModuleWithProviders } from '@angular/core';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { isNull, isUndefined } from 'lodash';


export function HttpLoaderFactory(http: Http) {
    return new TranslateHttpLoader(http, '../../../assets/i18n/', '.json');
}

const translationOptions = {
    loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [Http]
    }
};

@NgModule({
    imports: [TranslateModule.forRoot(translationOptions)],
    exports: [TranslateModule],
    providers: [TranslateService]
})
export class TranslationConfigModule {

    private browserLang;

    /**
     * @param translate {TranslateService}
     */
    constructor(private translate: TranslateService) {
        // Setting up Translations
        translate.addLangs(['en', 'it']);
        translate.setDefaultLang('en');
        this.browserLang = translate.getBrowserLang();
        translate.use(this.browserLang.match(/en|it/) ? this.browserLang : 'en');
    }

    public getBrowserLang(){
        if(isUndefined(this.browserLang) || isNull(this.browserLang)){
            this.browserLang = 'en';
        }
        return this.browserLang;
    }
}

Ok, and now? How can I use such configuration?

Any Module/Component imported into app.module.ts or any of them that is imported into another custom module that is importing translation.config.module can now automatically translate any interpolated entry based on browser language. For instance using the following snipped will generate Welcome or Benvenuto based on explained behavior:

{{ 'TEST.WELCOME' | translate }}

What If I want to get a resource to initialize a certain array that will be passed to an *ngFor?

In any component, just do that inside the constructor:

. . .

// Just some model
public navigationLinks: NavigationLinkModel[];

constructor(private _config: AppConfig) {
    // PAGES.HOMEPAGE.SIDENAV.NAVIGATION contains such model data
    this.navigationLinks = 
    this._config.getResourceByKey('PAGES.HOMEPAGE.SIDENAV.NAVIGATION');
 }

Of course you can also combinate resources and configuration.