Dynamically change locale for DatePipe in Angular 2
Asked Answered
R

7

30

I'm making an Angular project where the user has the ability to switch languages. Is it possible to make the locale dynamic?

I have seen that you can add it in the NgModule, but I'm guessing it's not dynamic when I put it there? Or can I change it somehow through a service or something?

Redevelop answered 31/5, 2017 at 14:44 Comment(1)
For people wanting to change LOCALE_ID at runtime (not only at application startup), github.com/armanozak/angular-dynamic-locale is a wonderful solution (summarized in answers below).Radon
B
18

Using providers, you can change your default locale in your NgModule.

To do this, you need to import LOCALE_ID from angular/core and fetch your locale language to pass the same to providers.

import { LOCALE_ID } from '@angular/core';

@NgModule({
    imports: [//your imports],
    providers: [
        { provide: LOCALE_ID, useValue: "en-US" }
    ]
})

...
...
{
  provide: LOCALE_ID,
  deps: [SettingsService],      // Some service handling global settings
  useFactory: (settingsService) => settingsService.getLanguage()  // Returns the locale string
}
Biannual answered 31/5, 2017 at 15:22 Comment(7)
No not really, i would like to make the "en-US" a variable so that i can change it when the app is running.Redevelop
for that u can use any service for global settings and then add in providers, plz go through i have update the post.Biannual
Thanks for the response, this is what I needed!Redevelop
this solution only works once. getLanguage() only gets called one time. can i make it so that getLanguage gets called again when some property in my app gets changed?Redevelop
@MartijnvandenBergh did you find a solution? I have the same problemVilleinage
I got a better answer when I asked my question a little different (read: better) #45562046 Maybe this will help?Redevelop
@MartijnvandenBergh try any of workarounds for this issue: https://mcmap.net/q/467420/-dynamically-change-locale-for-datepipe-in-angular-2 I've used #4 because I have hybrid Angular - AngularJS appHaler
H
49

To set locale from a service, you need to add the LOCALE_ID provider with the factory to app.module, like in Amol Bhor's answer:

{
  provide: LOCALE_ID,
  deps: [SettingsService],      //some service handling global settings
  useFactory: (settingsService) => settingsService.getLanguage()  //returns locale string
}

Unfortunately, you cannot change the language for DatePipe JIT. The Angular compiler requires LOCALE_ID during bootstrapping.

There are some bug reports for Angular:

There are several workarounds for this:

Workaround #1

Re-bootstrapping the Angular module:

let _platformRef: NgModuleRef<Object>;
if(_platformRef) { _platformRef.destroy(); }
platformBrowserDynamic(providers)
    .bootstrapModule(AppModule, {providers})
    .then(platformRef => {
        _platformRef = platformRef;
    })

This won't work for Hybrid Angular/AngularJS as there isn't any way do destroy AngularJS using UpgradeModule.

Workaround #2

To overwrite DatePipe, NumberPipe - whatever you need:

@Pipe({name: 'datepipe', pure: true})
export class MyDatePipe implements PipeTransform {
  transform(value: any, pattern?: string): string | null {
    // transform value as you like (you can use moment.js and format by locale_id from your custom service)
    return DateUtils.format(value);
  }
}

Workaround #3

To use a library which already handles localization with custom Pipes, for example:

Workaround #4

Every pipe which uses LOCALE_ID has a private field locale or _locale, so you may override this field at those pipes on a language change, as there is one instance of the pipe.

That will work because TypeScript is just syntactic sugar for JavaScript. And in JavaScript there aren’t any private fields.

Also remember to process the change detection in the application by using the tick() method in ApplicationRef.

@Injectable()
export class DynamicLocaleService {
  private i18nPipes: PipeTransform[];

  constructor(
    datePipe: DatePipe,
    currencyPipe: CurrencyPipe,
    decimalPipe: DecimalPipe,
    percentPipe: PercentPipe,
    private applicationRef: ApplicationRef,
  ) {
    this.i18nPipes = [
      datePipe,
      currencyPipe,
      decimalPipe,
      percentPipe,
    ]
  }

  setLocale(lang: string): void {
    this.i18nPipes.forEach(pipe => {
      if(pipe.hasOwnProperty("locale")) {
        pipe["locale"] = lang;
      } else if (pipe.hasOwnProperty("_locale")) {
        pipe["_locale"] = lang
      }
    })
    this.applicationRef.tick()
  }
}

Workaround #5

To reload the application when the language is changed.

window.location.reload()

Unfortunately, all of the above are workarounds.

But there is also another solution. You can have multiple bundles for each language, which probably will be a better approach as the app will be faster. But this solution is not applicable for every application and doesn't answer the question.

Haler answered 5/4, 2018 at 15:8 Comment(7)
All locales must be imported and registered in main.ts file: ``` import { registerLocaleData } from '@angular/common'; import locale_EN from '@angular/common/locales/en'; import locale_RU from '@angular/common/locales/ru'; registerLocaleData(locale_EN); registerLocaleData(locale_RU); ```Kristakristal
Hmm I would like to use workaround #4 but it is not working: Even when I use the setLocale function on ngOnInit it does not change the currency from dollars to euros in my view. Angular 7. Am I missing something? outside of that code I did what @Kristakristal mentioned and added the pipes as providers to a sharedModule. Locales are correctly registered because I can manually set a locale within the pipe itself.Domela
@Domela locale doesn't change your currency, but a way of displaying it. to change currency, pass proper value to CurrencyPipe. for ex. in html: {{ amount | currency: "EUR" }}Haler
Thanks for answering Anton but I was hoping by changing the locale it would change from a $0.50 to €0,50 (notice the comma). ofcourse I could do it that way but in a multi-country app I am looking for global solutions and still use the default pipes as much as possible.Domela
@Domela for sure all displaying rules based on locale like dot/comma, side of currency sign is managed by currency pipe. But I hope you understand that $0.50 != €0,50, so you should provide proper amount & currency to CurrencyPipe. Because CurrencyPipe doesn't have currency exchange out of the box. For sure you can create your own currency exchange pipe, which can use external exchange rates api, or your hardcoded rates, locale, and currency pipeHaler
Ahh Anton you misunderstood, I was not talking about converting from an actual $0.50 to €0,50 but just the display of it: using a locale of 'nl' for DecimalPipe for example makes sure it contains a comma instead of a dot. But currently that is hardcoded which I would like to do dynamically, globally.Domela
An addition to #4: If it doesn't work for you and you're using UI-Router, try StateService.reload() instead of applicationRef.Tick()Debbydebee
B
18

Using providers, you can change your default locale in your NgModule.

To do this, you need to import LOCALE_ID from angular/core and fetch your locale language to pass the same to providers.

import { LOCALE_ID } from '@angular/core';

@NgModule({
    imports: [//your imports],
    providers: [
        { provide: LOCALE_ID, useValue: "en-US" }
    ]
})

...
...
{
  provide: LOCALE_ID,
  deps: [SettingsService],      // Some service handling global settings
  useFactory: (settingsService) => settingsService.getLanguage()  // Returns the locale string
}
Biannual answered 31/5, 2017 at 15:22 Comment(7)
No not really, i would like to make the "en-US" a variable so that i can change it when the app is running.Redevelop
for that u can use any service for global settings and then add in providers, plz go through i have update the post.Biannual
Thanks for the response, this is what I needed!Redevelop
this solution only works once. getLanguage() only gets called one time. can i make it so that getLanguage gets called again when some property in my app gets changed?Redevelop
@MartijnvandenBergh did you find a solution? I have the same problemVilleinage
I got a better answer when I asked my question a little different (read: better) #45562046 Maybe this will help?Redevelop
@MartijnvandenBergh try any of workarounds for this issue: https://mcmap.net/q/467420/-dynamically-change-locale-for-datepipe-in-angular-2 I've used #4 because I have hybrid Angular - AngularJS appHaler
K
6

Have your Service like:

import { Injectable } from '@angular/core';

@Injectable()
export class LocaleService {

  // Choose the locale from this link:
  //https://github.com/angular/angular/tree/master/packages/common/locales
  constructor() { }

  private _locale: string;

  set locale(value: string) {
    this._locale = value;
  }
  get locale(): string {
    return this._locale || 'en-US';
  }

  public registerCulture(culture: string) {
    debugger;
    if (!culture) {
      return;
    }
    switch (culture) {
      case 'en-uk': {
        this._locale = 'en';
        console.log('Application Culture Set to English');
        break;
      }
      case 'zh-hk': {
        this._locale = 'zh-Hant';
        console.log('Application Culture Set to Traditional Chinese');
        break;
      }
      case 'zh-cn': {
        this._locale = 'zh-Hans';
        console.log('Application Culture Set to Simplified Chinese');
        break;
      }
      default: {
        this._locale = 'en';
        console.log('Application Culture Set to English');
        break;
      }
    }
  }
}

And in file App.module.ts:

First import localization that you need, say

import localeEN from '@angular/common/locales/en';
import localezhHant from '@angular/common/locales/zh-Hant';
import localezhHans from '@angular/common/locales/zh-Hans';

Then under the providers section

{
  provide: LOCALE_ID,
  deps: [LocaleService],
  useFactory: (LocaleService: { locale: string; }) => LocaleService.locale
}

At the end

registerLocaleData(localeEN);
registerLocaleData(localezhHant);
registerLocaleData(localezhHans);

If you want to change the locale dynamically, inject LocaleService in your desired component and use the registerCulture method, and pass your required culture into this.

Knave answered 28/8, 2019 at 11:38 Comment(0)
M
4

Great answers are already provided here! However, it did not completely satisfy my scenario working in a hybrid AngularJS/Angular environment.

Here's my solution that includes aspects from the previous answers with an alternate approach for importing of locales using dynamic import making bundling more efficient via lazy loading.

Summary:

Key points (also included in previous answers)

  • The LOCALE_ID is configured using the localization service as setup in the app.module.ts provider, via useFactory option
  • The registerLocaleData function registers the locale data globally

Extended implementation points (not included in previous answers)

  • The registerLocaleData function requires the import of the locale which in previous answers is included hard-coded and results in bundling of each locale:

    `import localeEN from '@angular/common/locales/en';
    

    We can use dynamic loading (available as of TypeScript 2.4) to load a given locale on demand, making our code and bundling more efficient. The import returns a Promise and we can then register our locale:

    import(`@angular/common/locales/${localeId}.js`)
    .then(lang => registerLocaleData(lang.default));
    
  • To improve bundling even more, we can include some magic comments to limit to only the locale we support:

    /* webpackInclude: /(en|fr|es)\.js$/ */

  • To take advantage of dynamic import we must configure our module type to esnext, see tsconfig.json

  • You can read about dynamic import and webpack magic comments here: https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import

Code:

app.module.ts

@NgModule({
    declarations: [ /* ... */ ],
    imports: [ /* ... */ ],
    providers: [
        { provide: LOCALE_ID, deps: [LocalizationService], useFactory: (localizationService) => localizationService.getLocale() }
    ]
})

localization.service.ts

export class LocalizationService {

    /**
     * Gets the current locale (abbreviation)
    */
    getLocale() : string {
        return localStorage.getItem("current-locale");
    }

    /**
     * Set the locale across the app
     * @param {string} abbr Abbreviation of the locale to set
     */
    setLocale(abbr : string) : Promise<any> {
        return new Promise(resolve => {
            return this.http.get<Translation[]>(url)
                .subscribe((response) => {
                    // Code omitted to load response data into the translation cache

                    if (localStorage) {
                        localStorage.setItem("current-locale", abbr);
                    }

                    moment.locale(abbr);
                    this.loadLocale(abbr).then;

                    resolve();
                },
                (error) => {
                    resolve;
                });
        });
    }

    /**
     * Imports the Angular data for the given locale
     * @param {string} localeId The locale to load data
     */
    private loadLocale(localeId : string) : Promise<any> {
        localeId = localeId.substr(0, 2);

        // Limit loading to the languages defined in webpack comment below
        return import(
            /* webpackInclude: /(en|fr|es)\.js$/ */
            `@angular/common/locales/${localeId}.js`
        ).then(lang =>
            registerLocaleData(lang.default)
        );
    }
}

tsconfig.json

    "compilerOptions": {
        /* ... */
        "module": "esnext"
        /* ... */
    }
Mayan answered 25/2, 2021 at 16:44 Comment(1)
import() syntax AND Webpack magic comments need to be corrected for Angular 13+. github.com/angular/angular-cli/issues/…Mount
S
1

The ultimate solution for me is this: Dynamic Locale in Angular

I tried it with Angular 12, and it allows dynamic change of LOCALE_ID without reloading the app.

Sokil answered 4/1, 2022 at 9:11 Comment(0)
A
0

This solution works for me:

loadLocales(localeId) {
    import(
    /* webpackExclude: /\.d\.ts$/ */
    /* webpackMode: "lazy-once" */
    /* webpackChunkName: "i18n-extra" */
    
    `@/../node_modules/@angular/common/locales/${localeId}`)
        .then(module => {registerLocaleData(module.default)});
}

For calling the function in a component:

this.loadLocales('de') // fr
            
// necessary import
import {registerLocaleData} from '@angular/common';
import {  APP_INITIALIZER } from '@angular/core';
            
            
//Add provider      
providers: [
    {
        provide: APP_INITIALIZER,
        useFactory: TestFunc,
        multi: true,
        deps: [CommonService]
    }
]
        
culture = service.culture; //'de-DE'
        
// for template reference
{{ showDateStart | date: "EEE, MMMM d, y, HH:mm": '' : culture}} //culture

If you define it for your application, you have to add it into app.module.ts.

Antakiya answered 20/7, 2022 at 12:29 Comment(1)
import() syntax AND Webpack magic comments need to be corrected for Angular 13+. github.com/angular/angular-cli/issues/…Mount
R
0

As mentioned by Claudio, Dynamic Locale in Angular is an excellent way, probably the most elegant, to change the value of LOCALE_ID—used by third-party pipes—at runtime (not only at application initialization). As it's an external link, I'll summarize here.

As other answers suggest, creating a LocaleProvider based on a LocaleService is a great way to dynamically control the value of LOCALE_ID.

import { LOCALE_ID, Provider } from '@angular/core';
import { LocaleService } from './locale.service';

/**
 * An way to provide LOCALE_ID via a Service.
 */
export class LocaleId extends String {
  constructor(private localeService: LocaleService) {
    super();
  }

  override toString(): string {
    return this.localeService.currentLocale;
  }

  override valueOf(): string {
    return this.toString();
  }
}

/**
 * Provides [[LOCALE_ID]] via [[LocaleService]].
 */
export const LocaleProvider: Provider = {
  provide: LOCALE_ID,
  useClass: LocaleId,
  deps: [LocaleService],
};

Now LocaleService is where you'll update the value of desired LOCALE_ID.

The problem is that your components and directives will already have been created: LOCALE_ID has already been injected to them with the value it had at the time of creation.

Now armanozak uses a technique, renavigating to the route, which RECREATES all components and pipes. He uses it when dynamically changing the LOCALE_ID, thus effectively updating it through the whole application.

import { Injectable, Optional, SkipSelf } from '@angular/core';
import { Router } from '@angular/router';
import { Subscription, noop } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class LocaleService {
  private locale: string;

  get currentLocale(): string {
    return this.locale;
  }

  constructor(
    private router: Router,
    @Optional() @SkipSelf() otherInstance: LocaleService,
  ) {
    if (otherInstance) throw 'LocaleService should have only one instance.';
    this.initLocale();
  }

  setLocale(localeId: string): void {
    if (!localeId) return;
    this.locale = localeId;
    void this.refreshApplicationLocaleId();
  }

  private initLocale(): void {
    // Init with browser's locale
    const locale = navigator.language;
    // Here, do any initialization necessary (fetching in LocalStorage, from a setting service, etc.)...
    this.setLocale(locale);
  }

  /**
   * Renavigate to current URL, forcing all directives to re-create.
   */
  private async refreshApplicationLocaleId(): Promise<void> {
    const fnShouldReuseRoute = this.router.routeReuseStrategy.shouldReuseRoute;
    this.router.routeReuseStrategy.shouldReuseRoute = () => false;
    this.router.navigated = false;
    await this.router.navigateByUrl(this.router.url).catch(noop);
    this.router.routeReuseStrategy.shouldReuseRoute = fnShouldReuseRoute;
  }
}

Note that, as components will be re-created, they will lose their state. This solution has been tested on Angular 13.

Note when using Cypress: when playing end-to-end tests with Cypress, it may not sync the visited URL under test with Angular's Router or Location. If that's the case, any call to router.url or location.path() would return '/', and the reloading trick above will navigate to the route application, making Cypress tests very likely to fail. It could be necessary then to use this snippet, using window.location.href:

  private async refreshApplicationLocaleId(): Promise<void> {
    const fnShouldReuseRoute = this.router.routeReuseStrategy.shouldReuseRoute;
    this.router.routeReuseStrategy.shouldReuseRoute = () => false;
    this.router.navigated = false;
    const fullPath = window.location.href;
    const currentUrl = fullPath.substring(fullPath.indexOf('/', fullPath.indexOf('/', fullPath.indexOf('/') + 1) + 1));
    await this.router.navigateByUrl(currentUrl).catch(noop);
    this.router.routeReuseStrategy.shouldReuseRoute = fnShouldReuseRoute;
  }
Radon answered 21/6, 2023 at 12:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.