Deliver web apps with confidence 🚀
Find a file
Doug Parker 5a0ff41f75 refactor(compiler): ensure context is always provided for WhitespaceVisitor (#56507)
When disabling `i18nPreserveSignificantWhitespaceForLegacyExtraction` I was looking at a test case with ICU messages containing leading and trailing whitespace:

```angular
<div i18n>
  {apples, plural, =other {I have many apples.}}
</div>
```

This would historically generate two messages:

```javascript
const MSG_TMP = goog.getMsg('{apples, plural, =other {I have many apples.}}');
const MSG_FOO = goog.getMsg(' {$ICU} ', { 'ICU': MSG_TMP });
```

But I found that I was getting just one message:

```javascript
const MSG_TMP = goog.getMsg(' {apples, plural, =other {I have many apples.}} ');
```

This is arguably an improvement, but changed the messages and message IDs, which isn't desirable with this option. I eventually traced this back to the `isIcu` initialization in [`i18n_parser.ts`](/packages/compiler/src/i18n/i18n_parser.ts):

```typescript
const context: I18nMessageVisitorContext = {
  isIcu: nodes.length == 1 && nodes[0] instanceof html.Expansion,
  // ...
};
```

[`_I18nVisitor.prototype.visitExpansion`](/packages/compiler/src/i18n/i18n_parser.ts) uses this to decide whether or not to generate a sub-message for a given ICU expansion:

```typescript
if (context.isIcu || context.icuDepth > 0) {
  // Returns an ICU node when:
  // - the message (vs a part of the message) is an ICU message, or
  // - the ICU message is nested.
  const expPh = context.placeholderRegistry.getUniquePlaceholder(`VAR_${icu.type}`);
  i18nIcu.expressionPlaceholder = expPh;
  context.placeholderToContent[expPh] = {
    text: icu.switchValue,
    sourceSpan: icu.switchValueSourceSpan,
  };
  return context.visitNodeFn(icu, i18nIcu);
}

// Else returns a placeholder
// ICU placeholders should not be replaced with their original content but with the their
// translations.
// TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = context.placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
context.placeholderToMessage[phName] = this.toI18nMessage([icu], '', '', '', undefined);
const node = new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
return context.visitNodeFn(icu, node);
```

Note that `isIcu` is the key condition between these two cases and depends on whether or not the ICU expansion has any siblings. The introduction of `WhitespaceVisitor` to `I18nMetaVisitor` trims insignificant whitespace, including empty text nodes not adjacent to an ICU expansion (from [`WhitespaceVisitor.prototype.visitText`](/packages/compiler/src/ml_parser/html_whitespaces.ts)):

```typescript
const isNotBlank = text.value.match(NO_WS_REGEXP);
const hasExpansionSibling =
  context && (context.prev instanceof html.Expansion || context.next instanceof html.Expansion);

if (isNotBlank || hasExpansionSibling) {
  // Transform node by trimming it...
  return trimmedNode;
}

return null; // Drop node which is empty and has no ICU expansion sibling.
```

`hasExpansionSibling` was intended to retain empty text nodes leading or trailing an ICU expansion, however `context` was `undefined`, so this check failed and the leading / trailing text nodes were dropped. This resulted in trimming the ICU text by dropping the leading / trailing whitespace nodes. Having only a single ICU expansion with no leading / trailing text nodes caused `_I18nVisitor` to initialize `isIcu` incorrectly and caused it to generate one message instead of two.

`WhitespaceVisitor` is supposed to get this context from `visitAllWithSiblings`. So the fix here is to make sure `WhitespaceVisitor` is always visited via this function which provides the required context. I updated all usage sites to make sure this context is use consistently and implemented the `WhitespaceVisitor.prototype.visit` method to throw when the context is missing to make sure we don't encounter a similar mistake in the future.

Unfortunately this broke one compliance test. Specifically the [`icu_logic/icu_only.js`](/home/douglasparker/Source/ng/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/icu_only.js) test which changed from generating:

```javascript
function MyComponent_Template(rf, ctx) {
  if (rf & 1) {
    i0.ɵɵi18n(0, 0);
  }
  // ...
}
```

To now generating:

```javascript
function MyComponent_Template(rf, ctx) {
  if (rf & 1) {
    i0.ɵɵtext(0, " ");
    i0.ɵɵi18n(1, 0);
    i0.ɵɵtext(2, "\n");
  }
  // ...
}
```

This test uses the default value `preserveWhitespaces: false` (`i18nPreserveSignificantWhitespaceForLegacyExtraction` should not affect compiled JS output, we already retain significant whitespace there). So what this indicates to me is that ICU logic is already broken because it's not preserving significant whitespace in this case. My change is probably a bug fix, but one which would affect the compiled runtime, which is not in scope here. The root cause is because using `visitAllWithSiblings` everywhere means the context is retained correctly in this case and the whitespace is leading/trailing an ICU message, therefore it is retained per the logic of `WhitespaceVisitor.prototype.visitText` I mentioned eariler.

To address this, I left one usage of `WhitespaceVisitor` using `html.visitAll` instead of `visitAllWithSiblings` to retain this bug. I has to lossen the assertion I put in `WhitespaceVisitor.prototype.visit` to make this possible, but it should still throw by default when misused, which is the important part.

PR Close #56507
2024-08-27 13:13:56 -07:00
.circleci refactor(docs-infra): complete removal of aio directory (#56496) 2024-06-18 12:26:00 -07:00
.devcontainer refactor(docs-infra): complete removal of aio directory (#56496) 2024-06-18 12:26:00 -07:00
.github build: update github/codeql-action action to v3.26.3 (#57455) 2024-08-20 13:20:01 -07:00
.husky build: simplify husky setup (#54315) 2024-02-07 16:34:13 +00:00
.ng-dev refactor(docs-infra): complete removal of aio directory (#56496) 2024-06-18 12:26:00 -07:00
.vscode refactor(docs-infra): complete removal of aio directory (#56496) 2024-06-18 12:26:00 -07:00
.yarn build: update yarn (#50732) 2023-06-16 10:51:09 +02:00
adev docs(docs-infra): Add query as an url query param to the api reference (#57062) 2024-08-27 12:58:00 -07:00
contributing-docs docs: add docs authoring guide (#56505) 2024-07-31 13:58:53 +00:00
devtools fix(devtools): catch invalidated extension error to prevent devtools from spamming console (#55697) 2024-08-27 12:54:44 -07:00
goldens refactor(compiler): add i18nPreserveWhitespaceForLegacyExtraction (#56507) 2024-08-27 13:13:56 -07:00
integration build: update to TypeScript 5.6 RC (#57507) 2024-08-26 09:13:52 -07:00
modules refactor: add select scenario to the js-web-frameworks benchmark (#56362) 2024-06-13 08:59:18 -07:00
packages refactor(compiler): ensure context is always provided for WhitespaceVisitor (#56507) 2024-08-27 13:13:56 -07:00
scripts build: replace deprecated inquirer with new npm package (#57205) 2024-07-31 16:02:11 +00:00
third_party build: create NodeJS ESM loader for supporting Bazel setup (#48521) 2022-12-19 19:50:40 +00:00
tools fix(docs-infra): leverage http_server rule from @angular/build-tooling for adev local serving (#57427) 2024-08-19 09:18:47 -07:00
.bazelignore refactor(docs-infra): complete removal of aio directory (#56496) 2024-06-18 12:26:00 -07:00
.bazelrc fix(docs-infra): leverage http_server rule from @angular/build-tooling for adev local serving (#57427) 2024-08-19 09:18:47 -07:00
.bazelversion build: update to bazel v5 for new runfiles API used in dev-infra (#45407) 2022-03-21 16:55:36 -07:00
.clang-format feat(tooling): Add a .clang-format for automated JavaScript formatting. 2015-04-02 08:44:34 -07:00
.editorconfig build: use https link to editorconfig.org in .editorconfig (#27664) 2018-12-18 09:30:09 -08:00
.gitattributes build: cleanup .gitattributes file and remove outdated CRLF attribute (#46513) 2022-06-28 13:38:27 -07:00
.gitignore build: add ignores for aio (#56538) 2024-06-21 10:03:34 -07:00
.gitmessage build: clean up references to old master branch (#45856) 2022-05-04 16:23:33 -07:00
.mailmap build: add a Git .mailmap with my new name (#19550) 2017-10-09 14:35:30 -07:00
.npmrc build: rely on engines to prevent using npm for dependency install (#41477) 2021-04-07 12:05:01 -07:00
.nvmrc build: bump to node 18.20 to support v18 (#55162) 2024-04-12 14:51:44 -07:00
.prettierrc ci: correct invalid attributes in prettierrc configuration (#56279) 2024-06-10 14:05:17 -07:00
.pullapprove.yml ci: add an entry for aio in the pullapprove config (#56952) 2024-07-11 17:03:26 -07:00
.yarnrc build: update yarn (#50732) 2023-06-16 10:51:09 +02:00
browser-providers.conf.d.ts build: share Saucelabs browsers between karma test targets using background Saucelabs daemon and custom karma launcher (#49200) 2023-05-15 09:21:46 -07:00
browser-providers.conf.js ci: Update Saucelabs config to use Andoid 13 and 14 (#55710) 2024-05-30 16:43:25 +00:00
BUILD.bazel refactor(docs-infra): complete removal of aio directory (#56496) 2024-06-18 12:26:00 -07:00
CHANGELOG.md docs: release notes for the v18.2.1 release 2024-08-22 09:03:04 -07:00
CHANGELOG_ARCHIVE.md docs: fix typos 2023-06-22 12:56:49 +02:00
CODE_OF_CONDUCT.md build: update CODE_OF_CONDUCT.md to match the content of angular/.github 2023-03-13 21:07:51 +00:00
CONTRIBUTING.md docs: change code formatting for better readability (#56974) 2024-07-15 12:05:43 -07:00
gulpfile.js ci: complete migration to prettier formatting (#55580) 2024-04-29 14:00:16 -07:00
karma-js.conf.js ci: complete migration to prettier formatting (#55580) 2024-04-29 14:00:16 -07:00
LICENSE docs: bump to 2024 for copyright text (#54822) 2024-03-11 13:39:38 -07:00
package.json build: update to TypeScript 5.6 RC (#57507) 2024-08-26 09:13:52 -07:00
packages.bzl refactor(docs-infra): migrate api-gen from dev-infra into the repo (#57241) 2024-08-05 17:06:29 +00:00
README.md docs: use new blog in links of new documentation (#56052) 2024-07-29 13:48:37 -07:00
renovate.json ci: replace matchDepPatterns with matchPackageNames (#57175) 2024-07-29 10:50:45 +02:00
SECURITY.md docs: update security guide link to adev (#56469) 2024-06-17 08:54:08 -07:00
tsconfig-tslint.json build: bump to node 18.20 to support v18 (#55162) 2024-04-12 14:51:44 -07:00
tslint.json refactor(docs-infra): complete removal of aio directory (#56496) 2024-06-18 12:26:00 -07:00
WORKSPACE docs(docs-infra): Use shiki for code highlighting (#57059) 2024-07-24 10:24:51 -07:00
yarn.bzl build: update yarn (#50732) 2023-06-16 10:51:09 +02:00
yarn.lock build: update to TypeScript 5.6 RC (#57507) 2024-08-26 09:13:52 -07:00

Angular - The modern web developer's platform

angular-logo
Angular is a development platform for building mobile and desktop web applications
using TypeScript/JavaScript and other languages.

angular.dev

Contributing Guidelines · Submit an Issue · Blog

CI status   Angular on npm   Discord conversation

InsightsSnapshot


Documentation

Get started with Angular, learn the fundamentals and explore advanced topics on our documentation website.

Advanced

Local Development

To contribute to the Angular Docs, check out the Angular.dev README

Development Setup

Prerequisites

Setting Up a Project

Install the Angular CLI globally:

npm install -g @angular/cli

Create workspace:

ng new [PROJECT NAME]

Run the application:

cd [PROJECT NAME]
ng serve

Angular is cross-platform, fast, scalable, has incredible tooling, and is loved by millions.

Quickstart

Get started in 5 minutes.

Ecosystem

angular ecosystem logos

Changelog

Learn about the latest improvements.

Upgrading

Check out our upgrade guide to find out the best way to upgrade your project.

Contributing

Contributing Guidelines

Read through our contributing guidelines to learn about our submission process, coding rules, and more.

Want to Help?

Want to report a bug, contribute some code, or improve the documentation? Excellent! Read up on our guidelines for contributing and then check out one of our issues labeled as help wanted or good first issue.

Code of Conduct

Help us keep Angular open and inclusive. Please read and follow our Code of Conduct.

Community

Join the conversation and help the community.

Love Angular badge

Love Angular? Give our repo a star ⬆️.