angular-cli: ng xi18n fails with library which uses Ivy

🐞 Bug report

Command (mark with an x)

  • new
  • build
  • serve
  • test
  • e2e
  • generate
  • add
  • update
  • lint
  • [ X ] xi18n
  • run
  • config
  • help
  • version
  • doc

Is this a regression?

No

Description

When running xi18n command on my application, that imports my library i get the following error:

ERROR: Unexpected value 'SharedModule in /Users/****/Code/my-app/dist/@shared/lib/shared.module.d.ts' imported by the module 'CoreModule in /Users/****/Code/my-app/libs/my-core/src/lib/core.module.ts'. Please add a @NgModule annotation.

But this is only happening when my lib is pre-build with Ivy option set to true, otherwise everything is fine.

A clear and concise description of the problem...

🔬 Minimal Reproduction

  • ng new my-app
  • ng g lib my-lib
  • build my-lib
  • import my-lib into my-apps module
  • run ng xi18n for my-app

🔥 Exception or Error




`ERROR: Unexpected value 'SharedModule in /Users/****/Code/my-app/dist/@shared/lib/shared.module.d.ts' imported by the module 'CoreModule in /Users/****/Code/my-app/libs/my-core/src/lib/core.module.ts'. Please add a @NgModule annotation.`

🌍 Your Environment




`@angular/* version 9.0.0-rc-4 or 0.900.0-rc-4`

Anything else relevant?

Nope

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 29 (9 by maintainers)

Commits related to this issue

Most upvoted comments

Hi @BenLune - I understand your frustration with this and I am sorry that we have not communicated clearly enough what is going on. I have tried to explain the situation in the various issues that have come up, but I can see that this is hard for developers to find when their project is not working as expected.

The documentation for i18n is currently being updated and I will see if we can get an info box that explains that, right now, the extraction mechanism is based on ViewEngine compiler, which is not able to cope with some code syntax shapes that the Ivy compiler can handle.

Regarding the use of $localize. I am sorry that you are frustrated by this. We have purposefully not documented this since for v9 and v10 it is an internal implementation device which was not ready to be used directly in Angular applications. I am afraid that there have been a number of blog postings and other comments where this has been talked abut as a new Ivy feature, which has confused the message.

Indeed using $localize in your own application code effectively works, but as is mentioned in numerous issues, which I have tried to respond to, there is currently no way to automatically extract these messages into translation files. This means that those people who have been trying to use this “internal” feature have felt disappointed. This was never our intention and why we did not publicly document this feature.

The fact that the current message extraction fails in certain cases is indeed a bug, which will be fixed by the new Ivy based message extractor, which I am actively working on. It has been my 2nd highest priority after ensuring that our compatibility compiler (ngcc) is working correctly, which has a large impact on all Ivy projects.

Finally, I would like to clear up the translation strategy that the Angular framework is offering here. Terms like “runtime translation” are ambiguous and mean different things to different people:

There are multiple pieces in the Angular translation offering:

$localize

This (currently internal) tool is a “tag handler” that can be applied to back-ticked strings to “mark” them as translatable. How that happens is dependent upon the project setup:

runtime translation

If we do no “translation inlining” during compilation then we must provide an implementation of this tag handler (by adding import '@angular/localize/init'; to your application). This handler will be executed every time the $localize tagged back-ticked string is evaluated.

By default this implementation is just a pass-through function that does not modify the text of the message. But if you were to load up some translations, by calling the (still internal) loadTranslations() function the implementation of $localize will actually replace the original text with the translated text.

What this means in practice is that in your own application code you could use $localize and as long as your code re-evaluates the back-ticked strings at the appropriate time you could indeed change the translations (i.e. the language) at runtime. But see below about why this doesn’t work for translations in templates…

compile-time inlining

In many cases we do not want to be doing the work of translating the messages at runtime so we have in place (via the CLI tool, and also a standalone binary localize-translate) tooling that will actually parse through the built JS source code and replace any instance of a $localize back-ticked string with a new back-ticked string, which has the $localize tag handler removed and all the text replaced with the translated text.

The result of this is code that no longer has any reference to $localize which keeps the code small and fast, and means we no longer need to have the $localize implementation mentioned above in the distributable code downloaded to the browser. All of this results in performance benefits across the board for the application.

Obviously, in this case, we must generate a separate copy of the source code for each language that your project supports, and there is no option to change language at runtime. The CLI is optimised to make this happen as fast as possible, by only running the translation as the last part of the build pipe-line avoiding having to run the earlier parts of the build multiple times for each language.

i18n in Angular templates

Everything about $localize above is internal. Currently the only publcly supported way to internationalise your application is via i18n tags. Here I want to clear up the Angular approach to i18n in the templates and why we believe it is not effective to enable the language in templates to be changed at runtime.

Previously, in the View Engine compiler, we would always inline the chosen language at the point the component template is compiled. This is extremely early in the build pipeline and resulted in long builds where we had to run the complete pipeline for each language. Now that we have the option of using $localize for this purpose, which can be inlined much later in the pipeline, the Ivy compiler takes a different approach.

In the Ivy compiler, we translate the i18n tags in the templates into back-ticked string that are tagged with $localize. This means that we can delay the inlining of the translations until later in the build. The result of Ivy template compilation is a “template function” that contains Ivy rendering instructions and also references variables that hold the result of evaluating tagged back-ticked message strings. These message strings contain additional markers (�) that are used by the Ivy runtime to interleave interpolations {{...}} and actual HTML elements within the translated message. This is necessary because we support ICU messages that can contain both.

At runtime the Ivy rendering instructions will process the translated message string and process the special markers breaking up the string and inserting pieces of it into the correct place in the DOM.

In order to support changing the language of these translated messages at runtime we would need two support two things:

  • some method to tell template functions that their translated messages have changed
  • for the template function to be able to reprocess the new translated messages and move DOM elements around without breaking the bindings of data to the DOM.

While it is theoretically feasible (I think) to achieve this, we feel that it has negative impact:

  • it makes the Ivy rendering runtime much more complicated and therefore more likely to contain unwanted behaviour (bugs!) - and there may even be cases where this re-rendering is simply not possible without blowing away the entire component (and all of it children etc) which is effectively like reloading the page.
  • the size of the framework code is increased solely to support a very small number (we believe) of use cases.
  • the cost of tracking the changes to the language (and so the messages) will impact on the runtime speed of Angular in general.

Because of this the design of i18n tags in templates precludes changing the language during normal application execution. I have spoken to a number of developers about runtime language replacement. The initial reaction is often one of “this is an essential feature” but when we dig into the practical benefits there have been very few (if any) scenarios where a reload of the site is not acceptable. For the kind of message translation that most applications require users tend to choose one language to work in for the life of the application.

There are some scenarios where language is used in a more dynamic way. Perhaps a call centre application where the user needs to be given responses in the language of the caller. But this is not really about localisation of the application itself, it is part of the business rules - for example in when changing language for the caller, you would not expect the language of the menus or action buttons to change.


Finally, it might be worth considering that $localize, when it finally becomes public, could provide an application developer with a way to change some aspects of language at runtime without reloading. It would involve re-evaluating the back-ticked strings as needed - and the application developer can have control over that - but the application would then be responsible for getting these updated strings into the rendered DOM (perhaps via Pipes or Directives).

But for the foreseeable future there are no plans to support such dynamic language updates via the i18n tag mechanism in Angular templates.


I hope that this helps to clear up some of the confusion around $localize and I hope that I can get the new Ivy extraction tooling merged and released before the end of June so that you can get past the issues of the current ViewEngine extraction tooling. Please keep an eye on my PR for this to see the progress: #32912 : i18n - extraction tooling.

But @alan-agius4 suggests that the problem is actually that your local libraries have been built using Ivy, and so they are not usable when trying to compile the main app via ViewEngine. The two built formats are incompatible. (This is the reason that we have ngcc, which converts ViewEngine compiled code to Ivy compiled code. But we do not have a reverse, which converts Ivy compiled code to ViewEngine compiled code).

So the way to work around this, until the new Ivy i18n message extractor is released, is to recompile your local libraries with ViewEngine before running the extraction.

Hello, It would be appreciated if we had a banner, at the top of this page, saying it doesn’t work yet with Ivy… with a link to the issue to check the state of it.

Thanks for the additional feedback @BenLune - it is really helpful in guiding the design of the i18n features.

As we create PWAs, it’s quite strange for me to reload the app just to switch language.

I would be really interested in some metrics of how often users switch language in your PWAs.

Then the ability, especially for the contributors to quickly change languages, and check if everything is ok, is really comfortable.

If I understand this correctly your are talking about some kind of “preview” in the backend of what the front end content will look like, yes? If so then I would not expect the backend user to want to change the actual language of the backend app. Instead they would just change the language of the preview, yes? In this case, I would imagine having the previous in some kind of iframe, which can be reloaded when you switch preview language, while the actual backend app itself remains in a single language. Am I misunderstanding the workflow here?

Finally, we may put our application interface terms in Drupal and load them runtime, maybe with a static export embedded in the application, then updated in IndexedDB when there is network.

This can actually be achieved with the $localize design right now. In fact I believe that https://www.locl.app/ has support, wrapped around $localize to do just this. The basic idea is that you do “runtime” merging of translations at the point the application loads - where the translated messages can be downloaded on-the-fly (or accessed from a local DB) before the application boots. (This is rather than inlining the translations at compile-time.)

Chiming in @alxhub as those errors are from the compiler.

We are targeting a fix for this in 9.1.0. There is already a draft PR in the framework side https://github.com/angular/angular/pull/32912