How to Embed Angular Components Inside a Transloco Translation String

Or how to use parameterized translations inside parameterized translations

Fredrik Holm
Sep 5, 2020 · 6 min read
Photo by Iza Gawrych on Unsplash

“What? Isn’t the application available in Swedish?”

was talking to my accountant on the phone, but I could literally hear him cringe none the less.

Earlier that week, I had shown it to my stepfather and gotten the same reaction from him. And come to think of, didn’t my buddy Mike make a remark about this as well?

So far, my application had been English only. With a global aim, it felt natural. And these days, most people are comfortable enough with the English language, so there shouldn’t really be necessary to translate it, right?

Think again.

Of course, it depends on what kind of application you are offering. But if you are targeting a traditional market segment as I do, you better face the music.

Flowmine, as my product is called, is an ERP application. It’s all about fun admin stuff like time-tracking, expense reporting, accounting, and invoicing.

And while I might beat a lot of my competition in terms of UX and general look and feel, they still had one thing I didn’t:

Internationalized applications. They were available in Swedish. Finnish. German.

While the fintech industry is evolving fast in terms of technology, it’s also very traditional when it comes to processes and terminology. At least many of the people who are using the products are.

And maybe rightly so — the financial domain can be very complex, and I fully understand that a user might be reluctant to learn a bunch of new English words just because the developers of the application were too lazy to translate it.

Let’s internationalize

So — long story short — I had to bite the bullet and internationalize Flowmine. The question remaining was how to do it.

Since the frontend is an Angular application, I basically had four options:

  • The official @angular/localize package
  • ngx-translate
  • Transloco
  • Roll my own

The official package is probably super-duper-powerful once you wrap your head around it, but it just didn’t click for me.

And while I had read a lot of positive things about ngx-translate in the past, the word “discontinued” popped up one time too many for me to be comfortable with it.

Having rolled my own translation framework for the backend, I was leaning towards that route.

But then I watched the video tutorials on Transloco by Shahar Kazaz and tried out the framework myself.

And what can I say it was love at first sight.

Today, I have spent around two weeks with Transloco, and I’m super impressed. Shahar, Netanel Basal, Itay Oded and the other guys contributing have made an awesome framework.

The only thing I missed was the ability to embed Angular components in translation strings, so here’s my solution for that.

The HTML embedding problem

A common internationalization problem is that you want to include markup inside your translation string. And while that works fine as long as it’s just simple markup, it does *not* work for Angular components (at least as long as you want to use AOT).

As the documentation puts it, we have two choices:

  • Transform your component into a web component using @angular/elements and use the HTML tag in your translation.
  • Separate your translation into two parts and insert the desired component between them.

For me, none of these options felt like a good fit for my use case. I simply wanted to be able to insert link tags with Angular attributes at arbitrary places in my translation strings, and simultaneously have the ability to use parameterized translations inside those tags.

Put another way:

Parameterized translations within parameterized translations.

Put yet another way, to produce output like this, where the link is embedded in the text:

Sure, you could break up the translation in multiple parts as suggested by the docs, but that would potentially harm readability. As soon as you introduce a language with a different sentence structure than your default language, you might be forced to use language like this:

<div>
This project is part of an invoice and cannot be edited.
<a […]>Click here to see the invoice</a>.
</div>

To me, this latter kind of language just doesn’t read as well as the former example, where the link is part of the natural flow of the language.

This leaves us with the web component option. But what if you don’t want to introduce another layer of complexity? (Or if you haven’t gotten around to learn web components properly yet, like me…)

There must be an easier way around this, right?

Sure. Albeit a bit hacky, we can solve it with a regular Angular component and some Vanilla JS. Here’s how I did it.

My solution — a custom translation component

Before diving into the source of the component, here’s what it looks like in action when it’s being used in the wild:

Listing 1, a real-world example

The interesting part of the above is the app-translate component, the rest is just included to provide some context. (It shows a warning to the user that the project is locked for editing since it’s already been billed.)

Just like a regular Transloco Translation API call, this component takes three arguments (in the form of attributes): key, params and scope.

This means that the actual translation is located in a scoped JSON file, like so:

/i18n/project/en.json

And inside that file, I have these keys:

"edit": {
"belongsToInvoice": {
"restrictionInfo": "This project is part of {{link}}. Hence, you can only edit estimated hours, deadline, status, and notes.",
"linkText": "invoice {{invoice}}",
"stillOpen": "Please note that you can still add tasks and track time on this project, as long as it's not marked as completed."
}

Notice that the {{link}} parameter in the translation file has a corresponding span tag in the gist in listing 1, with the attribute data-param=”link”.

What happens next is simply that the app-translate component injects the embedded span tag in the {{link}} placeholder, using content projection.

It looks like this:

Let’s begin with the HTML template. It defines a container and a target, where the former holds one or several embedded spans with data-param attributes inside the ng-content tag.

During component init, we set up an observable that combines Transloco’s langChanges$ with our own initialized$ subject.

The reason for this is to let Angular do its content projection magic before starting to move things around in the DOM.

Without the setTimeout() and ChangeDetectorRef’s detectChanges() calls, we would get the dreaded ExpressionChangedAfterItHasBeenCheckedError.

So, in ngAfterViewInit, we kick things off by emitting true to the initialized$ subject.

This, in turn, calls the renderParam() function for each and every embedded span.

We clear any existing children in the #target span by settings innerHTML to an empty string, an then we simply append clones of the child nodes in the containers. (The reason to use a span is semantic — the code we inject should probably be inline with the translation text.)

Why cloning instead of moving the originals?

It’s because that we want to preserve the originals. This is also the rationale for hiding the #container div with display: none.

Upon every language change we need to clone the originals again, and since those might contain translations of their own we need to wait for those to complete before cloning again.

What about the safeHtml pipe in the HTML template?

Nothing fancy about that one, here’s the source:

import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Pipe({ name: 'safeHtml' })
export class SafeHtmlPipe implements PipeTransform {

constructor(private sanitizer: DomSanitizer) { }

transform(value: any, ...args: any[]): any {
return this.sanitizer.bypassSecurityTrustHtml(value);
}
}

Let’s wrap it up

Like I hinted earlier, I haven’t read up enough on web components to judge if this solution is warranted or not. But if you want something working fast with the tools you already use (Angular and Vanilla JS), then this solution might be a good fit for you.

The Startup

Get smarter at building your thing. Join The Startup’s +793K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Fredrik Holm

Written by

Founder of Flowmine: https://flowmine.com. Building SaaS products and consulting in web development, solution architecture, and agile transformations.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +793K followers.

Fredrik Holm

Written by

Founder of Flowmine: https://flowmine.com. Building SaaS products and consulting in web development, solution architecture, and agile transformations.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +793K followers.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store