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'] = ['$'];
// in cr-language.js files, in the extend function
n['WLG'] = ['₩'];
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"
}
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?
RESOURCES
RELATED POSTS
Loading external configurations via http using APP_INITIALIZER
Top comments (0)