Sekrab Garage

Twisting Angular localization

Currency Angular pipe, UI language switch, and a verdict

Angular August 1, 22

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']
},

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' }}

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

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

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'] = ['$'];
				}
				
			}
		});
	
	}
};

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'] = ['$'];

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:
// in cr-language.js files, in the extend function
n['WLG'] = ['₩'];

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"
}

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>

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

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>
getServerLink(lang: string):string {
  // send a query param to server, of language and current URL
  return `/switchlang?lang=${lang}&red=${this.platform.doc.URL}`;
}

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

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>
getLanguageLink(lang: string): string {
  // replace current language with new language, add Res.language to res class
  return this.doc.URL.replace(`/${Res.language}/`, `/${lang}/`);
}

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';
  }
	// ...
}

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: 'عربي' },
    ]
  },
};

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
 >`

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"
	  // ...  
		>
 >`

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?

  1. 3 months ago
    Alternative way to localize in Angular
  2. 3 months ago
    Serving multilingual Angular application with ExpressJS
  3. two months ago
    Serving the same Angular build with different URLs
  4. two months ago
    Serving a different index.html in an Angular build for different languages
  5. two months ago
    Currency Angular pipe, UI language switch, and a verdict
  6. two months ago
    Pre-generating multiple index files using Angular Builders and Gulp tasks to serve a multilingual Angular app
  7. two months ago
    Using Angular APP_BASE_HREF token to serve multilingual apps, and hosting on Netlify
  8. two months ago
    Multilingual Angular App hosted on Firebase and Surge with the same build
  9. one month ago
    Alternative way to localized in Angular: Conclusion and final enhancements