DEV Community

Cover image for Currency Angular pipe, UI language switch, and a verdict
Ayyash
Ayyash

Posted on • Originally published at garage.sekrab.com

Currency Angular pipe, UI language switch, and a verdict

Angular i18n out of the box provides four pipes with localization: Date, Decimal, Percentage, and Currency. Of all four, I must confess, I thought the currency one was rubbish. I came to writing this article with the perception that it must be overridden. Let me explain.

Find the code in StackBlitz

The Angular currency pipe

Angular locale libraries do not list all currencies, the Russian locale for example lists only the following, which are the overriding values of the global currencies.

// provided currencies in a locale library
{
  'GEL': [u, ''],
  'RON': [u, 'L'],
  'RUB': [''],
  'RUR': ['р.'],
  'THB': ['฿'],
  'TMT': ['ТМТ'],
  'TWD': ['NT$'],
  'UAH': [''],
  'XXX': ['XXXX']
},
Enter fullscreen mode Exit fullscreen mode

The missing symbols and symbol-narrow

The scenario I was trying to fix is displaying the Turkish Lira symbol in a non Turkish locale, and it displayed with code "TRY." I thought it needed to be fixed, but the fix turned out to be simpler than I thought: symbol-narrow.

<!-- To show all currencies in their nice symbols, use symbol-narrow -->
{{ 0.25 | currency:SiteCurrency:'symbol-narrow' }}
Enter fullscreen mode Exit fullscreen mode

We can create our custom pipe that extends the current one, though I see no real value.

// extending the currency pipe
@Pipe({ name: 'crCurrency' })
export class CustomCurrencyPipe extends CurrencyPipe implements PipeTransform {
  transform(
    value: number | string | null | undefined,
    currencyCode?: string
  ): any {
    // get symbol-narrow by default
    return super.transform(value, currencyCode, 'symbol-narrow');
  }
}
Enter fullscreen mode Exit fullscreen mode

Looking at currencies in source code: the currencies list is pretty thorough, I do not quite understand the choices made for the first and second elements of every currency, but CLDR (Common Localization Data Repository), the library used to generate them, they have done a good job we do not wish to override.

But what if?

Overwriting locale's currency

One side effect of relying on locales, is that when we mean to always show the $ for all Australian dollars, one locale decides it should be AU$. The following is a rare scenario only to prove that it is doable; we can adapt the locale content.

Because we are making assumptions of the contents of a library, which might be updated one day, I do not advise this method in the long run.

First, the script in our language script (cr-ar.js, the one that loads the locale library). Let's wait for the script to load, and change the content:

// in cr-ar.js
(function (global) {
    if (window != null) {
      // in browser platform
      const script = document.createElement('script');
      script.type = 'text/javascript';
      script.defer = true;
      script.src = `locale/ar-JO.js`;
        script.onload = function () {
            // on load, add a missing currency symbol
            // TODO: write the extend function
            _extend();
      }
      document.head.appendChild(script);

    } else {
        // in server platform
        require(`./ar-JO.js`);
        // in server platform, simply call the function
        _extend();
    }
    // ...
})(typeof globalThis !== 'undefined' && globalThis || typeof global !== 'undefined' && global ||
  typeof window !== 'undefined' && window);
Enter fullscreen mode Exit fullscreen mode

The _extend function looks for the possible element in the array that holds the currency, and changes it. The only valid condition I find is that it is an object, and not an array.

// cr-ar.js, the extend function:
const _extend = function() {
    if (global.ng?.common?.locales) {
    // loop through the elements
        global.ng.common.locales['ar-jo'].forEach(n => {
        // it must be defined
            if (n){
                // is it an object but not an array, that's the one
                if (typeof n === 'object' && !Array.isArray(n)){
                    // change AUD to always show $ instead of AU$
                    n['AUD'] = ['$'];
                }

            }
        });

    }
};
Enter fullscreen mode Exit fullscreen mode

Currency verdict

My choices after tampering with it a bit:

  • If the project we are working on has a single currency, we can use the Decimal pipe with our preferred currency symbol
  • If we support multiple currencies, use the currency pipe as it is, with symbol-narrow
  • If we want to enforce a specific shape of a currency for all languages, the best option is to overwrite it in locale script.

Example scenario

Here is a scenario, I hope is not too unusual. A store in Sydney is targeting a local market for Japanese delivered merchandise, the audience are made up of three segments: Australians, and residents who speak Arabic and Japanese. The currencies are two: Australian dollars and Japanese Yens. We want our application to be translated to three languages but the currencies need to always be $ and ¥.

The issue is, using ar.js locale, the symbols look like this: AU$ and JP¥. Our choices are:

  • resolving to Decimal pipe and forcing our currency symbols
  • trusting the locale and leave it as it is (best choice)
  • overwriting it in our locale language script that does not display them correctly:
// in our extend function of cr-ar.js
n['JPY'] = ['¥'];
n['AUD'] = ['$'];
Enter fullscreen mode Exit fullscreen mode
// in cr-language.js files, in the extend function
n['WLG'] = [''];
Enter fullscreen mode Exit fullscreen mode

Goofing with a new currency

Since we are at it, what if we wanted to add Woolong currency to all locales?

  • Use Decimal pipe with our symbol  is probably the best way
  • Or extend the locales with a new currency, it's just as easy as the above:

But the English default locale does not have the global.ng.common available. For that, we find no option but to use the en.js locale in the cr-en.js file, and to replace our locale id with en instead of en-US. Don't forget to update angular.json assets array to bring in the en.js:

// assets json, bring in en.js
{
  "glob": "*(ar-JO|en).js",
  "input": "node_modules/@angular/common/locales/global",
  "output": "/locale"
}
Enter fullscreen mode Exit fullscreen mode

Have a look at the final result in StackBlitz.

UI Switch

Let's create a quick switch for both cookies and URL driven apps, to see if there is anything left to take care of. For the cookie only solution, the URL does not change when language changes, a simple browser reload is good enough.

Switch cookie in the browser

A simple switch with a button click. The cookie name needs to be maintained in the browser as well, and this is suitable for browser-only solutions.

<h5>Change cookie in the browser</h5>
<div class="spaced">
  <button class="btn" (click)="switchLanguage('ar')">عربي</button>
  <button class="btn" (click)="switchLanguage('en')">English</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Inject the proper platform and document tokens, and use configuration for the cookie name:

constructor(
  @Inject(PLATFORM_ID) private platformId: Object,
  @Inject(DOCUMENT) private doc: Document
) {
  // use token for document  and platform
}

switchLanguage(lang: string) {
  // cookie name should be saved in configuration cookie name: 'cr-lang'
  this.setCookie(lang, SomeConfig.cookiename, 365);
  this.doc.location.reload();
}
private setCookie(value: string, key: string, expires: number) {
  if (isPlatformBrowser(this.platformId)) {
    let cookieStr =
      encodeURIComponent(key) + '=' + encodeURIComponent(value) + ';';
    // expire in number of days
    const dtExpires = new Date(
      new Date().getTime() + expires * 1000 * 60 * 60 * 24
    );

    cookieStr += 'expires=' + dtExpires.toUTCString() + ';';
    // set the path on root to find it
    cookieStr += 'path=/;';

    document.cookie = cookieStr;
  }
}
Enter fullscreen mode Exit fullscreen mode

Switch cookie on the server

Making it server-platform friendly is a bit trickier. I can think of one value of making a browser cookie based solution work in a server-only platform, which is centralizing cookie management, and making it server-only. The way to do that is call an href, to a specific URL, with a redirect route in the path.

<h5>Change cookie on server</h5>
<a [href]="getServerLink('ar')">عربي</a>
<a [href]="getServerLink('en')">English</a>
Enter fullscreen mode Exit fullscreen mode
getServerLink(lang: string):string {
  // send a query param to server, of language and current URL
  return `/switchlang?lang=${lang}&red=${this.platform.doc.URL}`;
}
Enter fullscreen mode Exit fullscreen mode

The in express routes, redirect after saving the cookie:

app.get('/switchlang', (req, res) => {
    // save session of language then redirect
    res.cookie(config.langCookieName, req.query.lang, { expires: new Date(Date.now() + 31622444360) });
    res.redirect(req.query.red);
});
Enter fullscreen mode Exit fullscreen mode

Language URL

To change the language in the URL it better be an href, this is helpful for search crawlers.

<h5>Redirect to URL</h5>
<!--Probably in global config, we need to add all supported languages-->
<a [href]="getLanguageLink('ar')">عربي</a>
<a [href]="getLanguageLink('en')">English</a>
Enter fullscreen mode Exit fullscreen mode
getLanguageLink(lang: string): string {
  // replace current language with new language, add Res.language to res class
  return this.doc.URL.replace(`/${Res.language}/`, `/${lang}/`);
}
Enter fullscreen mode Exit fullscreen mode

This works for both browser and server platforms. And there is nothing more to do on the server. This is by far the sweetest solution. Let's add the language property to Res class:

// res class, add language property
export class Res {
  public static get language(): string {
      return cr.resources.language || 'en';
  }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Configure

In our config file, let's add the cookie name, and the supported languages. (You can make these part of an external configuration.)

// things to keep in config
export const Config = {
  Res: {
    cookieName: 'cr-lang',
    languages: [
      { name: 'en', display: 'English' },
      { name: 'ar', display: 'عربي' },
    ]
  },
};
Enter fullscreen mode Exit fullscreen mode

This makes the UI a bit simpler:

supportedlanguages = Config.Res.languages;
// in HTML template
`<a
    *ngFor="let language of supportedlanguages"
    [href]="getLanguageLink(language.name)"
    >{{ language.display }}</a
 >`
Enter fullscreen mode Exit fullscreen mode

There is one UX enhancement I would like to do; to highlight the currently selected language:

supportedlanguages = Config.Res.languages;
currentLanguage = Res.language;
// in HTML template
`<a
    // add this attribute
        [class.selected]="language.name === currentLanguage"
      // ...
        >
 >`
Enter fullscreen mode Exit fullscreen mode

I'm pretty sure you can think of more enhancements on your own, this could go on forever. Let's move on.

Generating different index files on build

It was easy to use express template engines, but the hype nowadays is making files statically ready, that is, to make the index.html file ready and serve it with no interpolation. My preferred way to accomplish that is a gulp task. But let's first experiment with Angular Builders. That is for the next episode. 😴

Have you Googled Woolong currency yet?

RESOURCES

RELATED POSTS

Loading external configurations via http using APP_INITIALIZER

Top comments (0)