New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i18n: Able to use translation strings outside a template #11405

marcalj opened this Issue Sep 7, 2016 · 183 comments



marcalj commented Sep 7, 2016

I'm submitting a ... (check one with "x")

[x] feature request

Current behavior
#9104 (comment)

I don't think it is possible, like I said before it only works with static text, it won't parse text on the js code, only templates

Expected/desired behavior
Be able to translate strings used anywhere in the code, using an API.

Reproduction of the problem

What is the expected behavior?
I'm referencing the usage of $translate.instant to exposure real use cases:

  • Custom rendered text:
if (data._current_results === data._total) {
                content = this.$translate.instant('CLIPPINGS__LIST__SUMMARY_ALL', {'num': data._current_results});
            } else {
                if (undefined === data._total) {
                    data._total = '...';

                content = this.$translate.instant('CLIPPINGS__LIST__SUMMARY', {
                    'num': data._current_results,
                    'total': data._total

            // Put 'mentions' first
            data = angular.merge({}, {
                mentions: mentions
            }, data);

            _.each(data, (value:number, key:string):void => {
                if (value) {
                    details += value + ' ' + this.$translate.instant('CLIPPINGS__LIST__SUMMARY_TYPE_' + key) + ', ';

            if (details) {
                details = '(' + _.trim(details, ', ') + ')';

            content = content.replace(':details', details);

More examples:

- Getting image's file name from exported image of a HTML rendered report:
getExportImageName(hideExtension:boolean):string {
        let fileName:string;

        fileName = this.$translate.instant('D_CHART_FACET_authors__EXPORT_FILENAME', {
            'profileName': this.ExportService.GetProfileName(),
            'period': this.ExportService.GetPeriodString(this.SearchFilter.GetPeriodFromInterval())

        if (!Boolean(hideExtension)) {
            fileName += '.png';

        return fileName;
  • Sometimes you're translating, sometimes use model data (could be very verbose in a template):
private _getTitle():string {
        if (this.inShareColumn) {
            return this.$translate.instant('COMPARISONS__SHARE_COLUMN_share_of_voice_TITLE');
        } else if (this.inTotalsColumn) {
            return this.$translate.instant('COMPARISONS__TOTAL_COLUMN_share_of_voice_TITLE');
        } else {
            return _.get<string>(, 'profileName', '');
  • Using a third party chart plugin (Highcharts)
this.chart = new Highcharts.Chart(<any>{
            title: {
                text: this.$translate.instant('REPORTS_BLOG_MAPPING_CHART_TITLE_tone').toUpperCase(),
            xAxis: {
                title: {
                    text: this.$translate.instant('REPORTS_BLOG_MAPPING_CHART_TITLE_tone_xaxis')
            yAxis: {
                min: 0,
                title: {
                    text: this.$translate.instant('REPORTS_BLOG_MAPPING_CHART_TITLE_tone_yaxis')
            plotOptions: {
                scatter: {
                    tooltip: {
                        headerFormat: '<b>{point.key}</b><br>',
                        pointFormat: '{point.y} ' + this.$translate.instant('REPORTS_BLOG_MAPPING_CHART_mentions')
  • To setup config variables and render the text without pipes as it's not always a translated string
this.config = {
            requiredList: true,
            bannedList: false,
            allowSpaces: false,
            allowComma: false,
            colorsType: false,
            defaultEnterAction: 'required',
            requiredTooltip: this.$translate.instant('D_CLIPPING_TAGS__REQUIRED_TOOLTIP'),
            bannedTooltip: this.$translate.instant('D_CLIPPING_TAGS__BANNED_TOOLTIP')
  • To set window.title :) :
SetWindowTitle(title:string) {
        if (!!title) {
            this.$window.document.title = this.$translate.instant(title);
  • Custom date formatting:
dateHuman(date:Date):string {
        return date.getDate() + ' ' + this.$translate.instant('GLOBAL_CALENDAR_MONTH_' + date.getMonth())
            + ' ' + date.getFullYear();
  • Sort things based on translated values:
// Sort types
            tmpTypes = _.sortBy(tmpTypes, (type:string):string => {
                // 'MISC' at the end
                if ('MISC' === type) {
                    return 'zzzzz';

                return this.$translate.instant('FACET_phrases2__TYPE_' + type);
GetSortedLanguages():IFacetLangDetectedCommonServiceLanguageObject[] {
        // We have to sort by translated languages!
        return _.sortBy(, (item:string):any => {
            return {
                key: item,
                label: this.$translate.instant('FACET_langDetected_' + item),
                cssStyle: (_.includes(['english', 'catalan', 'spanish', 'french', 'italian'], item))
                    ? {'font-weight': 'bold'} : null,
                flag: _.get(this.lutFlags, item, null)
        }), (item):string => {
            return item.label.toLowerCase();
  • Export raw data to CSV or Excel with translated values:
getDataExportStacked(inputData:any):any {
        let exportData = angular.copy(inputData);

        if (angular.isArray(exportData) && exportData.length) {
            exportData[0].name = this.$translate.instant('CLIPPINGS__CHARTS_volume_TITLE');

            exportData[0].data =[0].data, (inputDataItem:any):any => {
                return {
                    'label': inputDataItem.association.profileName,
                    'value': inputDataItem.value

        return exportData;
  • Set config strings to third party plugins:
UpdateCalendarStrings():void {
            lang: {
                months: [
                shortMonths: [
                weekdays: [
**What is the motivation / use case for changing the behavior?** Be able to translate strings outside templates.

Please tell us about your environment:

  • Angular version: 2.0.0-rc.6
  • Browser: [all]
  • Language: [TypeScript 2.0.2 | ES5 | SystemJS]


@marcalj marcalj referenced this issue Sep 7, 2016


[i18n] future 2.x plan #9104

13 of 21 tasks complete

This comment has been minimized.

Mattes83 commented Sep 20, 2016

I think this is a real showstopper, i18n is not ready to use until this is implemented.
E.g. I am not able to set translated validation messages in code


This comment has been minimized.

marcalj commented Sep 20, 2016

@Mattes83 Did you try this way?

<p [hidden]="!alertManagerRowForm.controls.sendTo.hasError('validateEmail')" class="help-error">
    {{ 'ALERTMANAGER__FORM__FIELD__sendTo__ERROR__validateEmail' | translate }}

This comment has been minimized.

manklu commented Sep 20, 2016

😕 The documentation tells you how nice it is to have error messages in code and translation support requires anything in the template. Looks like there is more need for communication.


This comment has been minimized.

Mattes83 commented Sep 21, 2016

@marcalj I know this way...but I need to have localized strings in my ts files.
@manklu I totally agree


This comment has been minimized.

rolandoldengarm commented Sep 30, 2016

I think this is a real showstopper, i18n is not ready to use until this is implemented.
E.g. I am not able to set translated validation messages in code

Yeah same here, I've just switched from ng2-translate to Angular2 i18n as I prefer to use OOTB modules, and it's much easier to extract translations (ng2-translate is more time consuming IMO)
At this stage I cannot translate error messages raised from my services. No workaround either.


This comment has been minimized.


vicb commented Sep 30, 2016

If someone want to start a design spec, it would be helpful (ie on Google Docs).

It would need to list all the cases. Quickly thinking about it, I see a need for

_.('static text');
_.('with {{ parameter }}', {parameter: "parameter" });
_.plural(count, {'few': '...'});, {'value': '...'});

This comment has been minimized.

fbobbio commented Oct 14, 2016

Hi guys, great feature request 👍

Is there any suggested workaround to translate *.ts texts?


This comment has been minimized.

mstawick commented Oct 17, 2016

@fbobbio What I've been doing is create hidden elements in the template, ex.
<span class="translation" #trans-foo i18n>foo</span>.

Bind them using:
@ViewChild('trans-foo) transFoo : ElementRef;.

Then retrieve translated value

Looks retarded, but works for my needs.


This comment has been minimized.

db6edr commented Oct 20, 2016

As ng-xi18n already processes the entire TS-code, why not implement a decorator like @i18n() for (string-)properties? These could then be filled with the translated value, like @Input() is used with one-way data binding.
If the untranslated value cannot easily be extracted from code, just place it in the argument like so:

@i18n( {
  source : 'Untranslated value',
  description: 'Some details for the translator'
} )
public set translatedProperty( value : string ) {
   this._translatedProperty = value;

and feed the source into the property when there is no translation target.


This comment has been minimized.

pharapiak commented Oct 20, 2016

This is an essential item in the whole internationalization cycle, IMO.

In my shop, we're accustomed to an "ng-xi18n - like" tool (an extension to xgettext) that crawls all types of source files looking for marked text to put into dictionary files for the translators.

I love the clean and easy i18n markup in the HTML templates, and I was expecting the same for Typescript code.


This comment has been minimized.

quanterion commented Nov 3, 2016

@vicb In addition to your use cases I'm thinking about possibility to support localization of interpolated string in TS code. However it's likely be needed to rewrite TS code to support such a scenario. Will it be a valid approach?


This comment has been minimized.

aaronleesmith commented Nov 4, 2016

This is the primary feature stopping us from using the out-of-the-box approach to translation Angular 2 offers. We have many metadata driven components whose keys come from some metadata not stored in HTML. If we could translate via pipe or programmatically against the available TRANSLATIONS, we could get these components to show the proper strings.

In the mean time, we are limited to something like ng-translate because of this limitation.


This comment has been minimized.

lvlbmeunier commented Nov 7, 2016

The way I worked around this problem is by adding an empty 'lang' object in a service used throughout my application. I then apply a directive that reads all span in a div and store the value in that object. I place all my string in a template at the bottom of the page with an hidden property. The string is then reachable from the template or the component. It's ugly and you could easily overwrite an entry with the same id. But it's better then nothing.


export class AppService {

    //Language string object.
    private _lang:Object = new Object();
    get lang():Object {
        return this._lang;


    selector: '[lang]'
export class LangDirective implements OnInit {

        public element: ElementRef,
        public app: AppService) {

    ngOnInit() {
        let ele = this.element.nativeElement;
        for (var i = 0; i < ele.children.length; i++) {
            let id = ele.children[i].getAttribute('id');
            let value = ele.children[i].innerHTML;


<div lang hidden >
    <span id="myButtonText" i18n="Test Button">Testing</span>

This comment has been minimized.

sknoth commented Dec 6, 2016

Hello @lvlbmeunier

Thank you for providing this suggestion for a workaround while we are waiting for the official implementation. I tried to implement your solution but I cannot seem to get the dynamic translation keys to get recognized. I was trying to do it like this:

<p *ngFor="let d of dataEntry">{{app.lang[]}}</p>
<div lang hidden >
<span id="{{}}" *ngFor="let d of dataEntry" i18n>{{}}</span>

Those new keys don't show up in my xliff files. Is it possible to achieve this with your solution?


This comment has been minimized.

lvlbmeunier commented Dec 6, 2016

I never tried it but i'm almost certain that the build of the xliff file does not run code. Having dynamic value in i18n would be contrary to the concept. If you know for certain of all the name that would be in your list they should all be declared independently and not in a for loop.


This comment has been minimized.

sknoth commented Dec 6, 2016

Adding the keys manually works but it is impractical. In my case I get hundreds of text keys in need of translation from an API.


This comment has been minimized.

lvlbmeunier commented Dec 6, 2016

You can run your code and take the source and copy it to a file. The generation of the x18n file is based on static file.


This comment has been minimized.

fredrikredflag commented Dec 20, 2016

With 4.0.0-beta you can assigning an id to i18n, for instance mainTitle:

<span i18n="title@@mainTitle">

With this we can, at least for the JIT compiler, create a dummy Component (it doesn't need to be added to the app html, just the app module) with all our "extra" translations.

For starters we'll add the providers not just to the compiler but to the app module as well:

// bootstrap.ts
getTranslationProviders().then(providers => {
    const options = { providers };
    // here we pass "options.providers" to "platformBrowserDynamic" as extra providers.
    // otherwise when we inject the token TRANSLATIONS it will be empty. The second argument of
   // "bootstrapModule" will assign the providers to the compiler and not our AppModule
    platformBrowserDynamic(<Provider[]>options.providers).bootstrapModule(AppModule, options);

Then we'll create a dummy component to house our extra translations, don't forget to add the component to declarations of AppModule. This is so that ng-xi18n can find the html (I think) and add it to the translation file.

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

    selector: 'tmpI18NComponent',
    templateUrl: 'tmp.i18n.html'
export class TmpI18NComponent {

Add our translations to tmp.i18n.html:

<!-- tmp.i18n.html -->
<span i18n="test@@mainTitle">
    test {{something}}

Now we can create a service where we can fetch our translations:

import {Injectable, Inject, TRANSLATIONS} from '@angular/core';
import {I18NHtmlParser, HtmlParser, Xliff} from '@angular/compiler';

export class I18NService {
    private _source: string;
    private _translations: {[name: string]: any};

        @Inject(TRANSLATIONS) source: string
    ) {
        let xliff = new Xliff();
        this._source = source;
        this._translations = xliff.load(this._source, '');

    get(key: string, interpolation: any[] = []) {
        let parser = new I18NHtmlParser(new HtmlParser(), this._source);
        let placeholders = this._getPlaceholders(this._translations[key]);
        let parseTree = parser.parse(`<div i18n="@@${key}">content ${this._wrapPlaceholders(placeholders).join(' ')}</div>`, 'someI18NUrl');

        return this._interpolate(parseTree.rootNodes[0]['children'][0].value, this._interpolationWithName(placeholders, interpolation));

    private _getPlaceholders(nodes: any[]): string[] {
        return nodes
            .filter((node) => node.hasOwnProperty('name'))
            .map((node) => `${}`);

    private _wrapPlaceholders(placeholders: string[]): string[] {
        return placeholders
            .map((node) => `{{${node}}}`);

    private _interpolationWithName(placeholders: string[], interpolation: any[]): {[name: string]: any} {
        let asObj = {};

        placeholders.forEach((name, index) => {
            asObj[name] = interpolation[index];

        return asObj;

    private _interpolate(pattern: string, interpolation: {[name: string]: any}) {
        let compiled = '';
        compiled += pattern.replace(/{{(\w+)}}/g, function (match, key) {
            if (interpolation[key] && typeof interpolation[key] === 'string') {
                match = match.replace(`{{${key}}}`, interpolation[key]);
            return match;

        return compiled;

Now we can do something like:

export class AppComponent {

    constructor(i18nService: I18NService) {
        // Here we pass value that should be interpolated in our tmp template as a array and 
        // not an object. This is due to the fact that interpolation in the translation files (xlf for instance)
        // are not named. They will have names such as `<x id="INTERPOLATION"/>
        // <x id="INTERPOLATION_1"/>`. And so on.
        console.log(i18nService.get('mainTitle', ['magic']));

This is a hacky workaround. But at least we can leverage the translation file, get by key, interpolate and not have to have a hidden html in our application.

NOTE: the current @angular/compiler-cli NPM package of 4.0.0-beta has an incorrect dependency version of @angular/tsc-wrapped. It points to 0.4.2 it should be 0.5.0. @vicb is this easily fixed? Or should we wait for next release?


This comment has been minimized.

ghidoz commented Dec 20, 2016

@fredrikredflag Awesome! And what about AOT?


This comment has been minimized.

fredrikredflag commented Dec 20, 2016

@ghidoz AOT is another story. What we would like to do is to precompile all translations so we can get them by key. But since the ngc will replace all i18n with the correct translation we can't leverage it. Can't find any exposed options/properties that contains the parsed translations from ngc. We could use the same dynamic approach as for JIT by fetching the xlt file that way still provide it on the TRANSLATION token. However this goes against the purpose of AOT.


/// bootstrap.aot.ts
function fetchLocale() {
    const locale = 'sv';
    const noProviders: Object[] = [];

    const translationFile = `./assets/locale/messages.${locale}.xlf`;
    return window['fetch'](translationFile)
        .then(resp => resp.text())
        .then( (translations: string ) => [
            { provide: TRANSLATIONS, useValue: translations },
            { provide: TRANSLATIONS_FORMAT, useValue: 'xlf' },
            { provide: LOCALE_ID, useValue: locale }
        .catch(() => noProviders);

fetchLocale().then(providers => {
    const options = { providers };

This comment has been minimized.

Invis1ble commented Jun 7, 2018

Without ability to translate outside templates i18n feature is completely useless for me.


This comment has been minimized.

aastrouski commented Jun 8, 2018

@ocombe Hello Olivier!
Any updates?



This comment has been minimized.

MickL commented Jun 8, 2018

If its done it wouldnt matter since we have to wait for Ivy to be complete, which is scheduled for Angular 7 (sept/oct 2018)


This comment has been minimized.

rarenal commented Jun 8, 2018

By Angular 7 this issue will be two years old LOL.
In any case Ivy has been the thing that has delayed this new feature the most...


This comment has been minimized.

vincentjames501 commented Jun 11, 2018

@ocombe , that's great to hear. We're currently stuck using a legacy i18n service we rolled from the ground up purely for this reason. Not being able to do it in JS makes it extremely hard to do some simple things for us:

  • Dynamic components
  • Integrating with 3rd party services where we have to provide some localized text to
  • Showing dynamic confirmation modals
  • Our api returns error types. In our context we need to dynamically localize these errors and a template driven approach would be kludgy.
  • We want to use the TitleService as the Angular team suggests, but there is no way to provide localized text!

I'm sort of curious how the Angular team deals with this currently...


This comment has been minimized.

dominionarq commented Jun 11, 2018

Hi @vincentjames501,
At our company we tried to do the same you are doing now, but it was very troublesome, so we ended up using the i18n-polyfill mentioned before in this thread, and so far it's working great.
The only drawback we have is the size of the bundles of our app. We have three translation files, and each bundle contains all of them inside (we haven't figured out a way to generate the bundle just with the translation it uses). But all of this are just nuisances that we hope will be resolved when Ivy is out.


This comment has been minimized.

ronzeidman commented Jun 11, 2018

I've included a translation map and reexported it in each language's environment.ts file. It works without issues, is not included in all bundles, and doesn't require an external library. The only issue I have is since evey site is different I can't have the same service worker for all languages and if someone switch languages my site cannot tell if he's already registered for push notifications or not...


This comment has been minimized.

nechamam commented Jun 12, 2018

@ocombe any news in angular 6?


This comment has been minimized.


ocombe commented Jun 12, 2018

It will not be in Angular v6, as explained above. We depend on Ivy which is planned for v7.


This comment has been minimized.


PowerKiKi commented Jun 12, 2018

@ocombe may I suggest that you lock this thread, so that only the team can give meaningful updates when appropriate and we avoid the repetitive questions that you keep answering ?


This comment has been minimized.

petersalomonsen commented Jun 13, 2018

My hack for use with the current Angular is using an ng-templateand create the embedded view programatically:

<ng-template #messagetotranslate i18n>This message should be translated</ng-template>

and then in in the component ts file:

@ViewChild('messagetotranslate') messagetotranslate: TemplateRef<any>;

createTranslatedMessage(): string {
  return this.messagetotranslate.createEmbeddedView({}).rootNodes[0].textContent;

The createTranslatedMessage method returns the translated message. Although I'm using the template to declare the message which is not optimal, this makes it possible for the CLI translate tool to register it in the xlf files, and I have a way of getting the translated message programmatically for use outside templates.


This comment has been minimized.

vupham-zingbox commented Jul 26, 2018

Hopefully the api will work for the routes as well ! For example

export const routes: Routes = [
  path: '',
  component: HomeComponent,
  data: {
   title: i18n('Home'),
   breadcrumb: i18n('Home')

This comment has been minimized.

tanekim77 commented Jul 29, 2018

If this feature is released, would there be a significant change from the current i18n api? Would you recommend I start my project with i18n now or should I wait for the new i18n Api when Angular 7 gets released?


This comment has been minimized.

shobhit12345 commented Jul 30, 2018

Any update When we will get run-time and dynamic translation support?


This comment has been minimized.


ocombe commented Aug 13, 2018

Runtime translations are done and merged (with ivy), but the compiler side is not finished yet.
I'm working on ICU expressions right now.
I haven't started to code the dynamic translations yet (service) but it's probably the next thing I'll do after ICU expressions.


This comment has been minimized.

shobhit12345 commented Aug 14, 2018

@vincentjames501 Can we use i18n-polyfills with ICU expression?i have get no solution how to implement that


This comment has been minimized.

djindjic commented Aug 14, 2018

@ocombe, can you please clarify the difference between runtime vs dynamic translations?


This comment has been minimized.


ocombe commented Aug 14, 2018

there's probably no official denomination, but the way I see it is:

  • runtime: translations are resolved at runtime, meaning not during build (like we do currently). It applies to all types of translations (templates, or in code)
  • dynamic means that you only know what you want to translate at runtime, and you couldn't do it at build time. It mostly applies to "in code" translations, using a service. I guess that you could consider ICU expressions dynamic in the sense that they depend on a variable, but you can still compute all of the possible translations at build time because the number of cases is finite.

This comment has been minimized.

andrei-tatar commented Aug 23, 2018

I postponed adding i18n support to the current project we're developing but we're getting closer to the release, any idea if this will be final in angular 7? Or should I consider other ways of adding translations?


This comment has been minimized.

MickL commented Aug 23, 2018

@andrei-tatar i18n is working perfectly since Angular 2. The only downsides:

  • No code translations (this issue) -> If you need it, use i18n-polyfill
  • If you build AOT(recommended), the app has to be built for each language separately
  • Since each app is built separately, when switching the language you have to reload the page

For the last two points you could use @ngx-translate instead. But it works differently from Angular's built in i18n so an later update could take some time. For the polyfill the update will be no time with probably no breaking changes.


This comment has been minimized.

shobhit12345 commented Aug 24, 2018


How we can handle conditional operator using Angular i18n feature

*<div class="show-history d-none d-md-block" ngIf="isOk">

{{paymentMethod.historyOpned ? "Hide" : "Show"}} History


This comment has been minimized.

Abekonge commented Aug 24, 2018


We use ngSwitch to template all messages, so we can attach i18n-tags to them individually.

    <ng-container [ngSwitch]="paymentMethod.historyOpned">
        <ng-container *ngSwitchCase="true" i18n="@@hide">Hide</ng-container>
        <ng-container *ngSwitchCase="false" i18n="@@show">Show</ng-container>
    <ng-container i18n="@@history">History</ng-container>

We use this method a lot to get around the missing dynamic translations. Given a bit of thought a surprising amount of text can be expressed in this way in templates. A common pattern is to have an array of messages, and then use this with ngFor and ngSwitch to template the messages, something like this:


messsages = [ 'this is the first message', 'this is the second message' ]


<ng-container *ngFor="let m of messages">
    <ng-container [ngSwitch]="m">
        <ng-container *ngSwitchCase="'this is the first message'" i18n="@@firstMesssage">This is the first message</ng-container>
        <ng-container *ngSwitchCase="'this is the second message'" i18n="@@secondMesssage">This is the second message</ng-container>

It's somewhat verboose - but it works!


This comment has been minimized.

shobhit12345 commented Aug 24, 2018


This comment has been minimized.

lizzymendivil commented Sep 30, 2018

If its done it wouldnt matter since we have to wait for Ivy to be complete, which is scheduled for Angular 7 (sept/oct 2018)

It's alraedy October. Do you think we'll have the new features by the end of October?


This comment has been minimized.


PowerKiKi commented Oct 1, 2018

@lizzymendivil according to Ivy is 65% complete and it seems unlikely to be completed in only 30 days


This comment has been minimized.


ocombe commented Oct 1, 2018

Yes, ivy will not be finished in one month.


This comment has been minimized.

JanEggers commented Oct 1, 2018

@ocombe can you please lock this down so only you can post updates. its a little annoying to get all the notifications of all the "when is it done?" comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment