Repository: ember-intl/ember-intl (ts)

Issue:
ICU arguments mismatch warning
The code in https://github.com/ember-intl/ember-intl/pull/1285 works great! We've gotten rid of 50 or so ICU mismatch warnings.

However, there is still one around:

> [ember-intl] "activemodel.errors.template.header" ICU argument mismatch: "de": "model"

That can be reproduced using these two YAML files:

```yml
# de.yml
header: >
  {count, plural,
    one {Ein Fehler hat die Speicherung von {model} verhindert}
    other {# Fehler hat verhindert, dass {model} gespeichert wurden}
  }

# en.yml
header: >
  {count, plural,
    one {An error}
    other {# errors}
  } prevented this {model} from being saved

```


***

### Environment

- Ember Version: 3.17.0
- Ember CLI Version: 3.17.0
- Ember Intl Version: 5.0.7
- Browser(s): Chrome 80.0.3987.163
- Node Version: 10.18.1
- OS: macOS Catalina


Pull request:
fix: Linter should traverse inside plural arguments.
fixes #1288

Patch:
diff --git a/lib/broccoli/translation-reducer/linter/extract-icu-arguments.js b/lib/broccoli/translation-reducer/linter/extract-icu-arguments.js
index afa70c56..bbb8e0fd 100644
--- a/lib/broccoli/translation-reducer/linter/extract-icu-arguments.js
+++ b/lib/broccoli/translation-reducer/linter/extract-icu-arguments.js
@@ -1,20 +1,22 @@
 const parser = require('intl-messageformat-parser');
+const traverse = require('../../../message-validator/ast-traverse');
 
 function extractICUArguments(string) {
   try {
+    const args = new Set();
+    const recordVisitedNodeValue = (node) => args.add(node.value);
+
     // Errors thrown by the parser won't include any details about the string. By rethrowing the
     // error with the string quoted it's much easier to identify which ICU argument was invalid.
-    return parser
-      .parse(string)
-      .filter(
-        (element) =>
-          parser.isTimeElement(element) ||
-          parser.isDateElement(element) ||
-          parser.isArgumentElement(element) ||
-          parser.isSelectElement(element) ||
-          parser.isPluralElement(element)
-      )
-      .map((argumentElement) => argumentElement.value);
+    traverse(parser.parse(string), {
+      [parser.TYPE.time]: recordVisitedNodeValue,
+      [parser.TYPE.date]: recordVisitedNodeValue,
+      [parser.TYPE.plural]: recordVisitedNodeValue,
+      [parser.TYPE.select]: recordVisitedNodeValue,
+      [parser.TYPE.argument]: recordVisitedNodeValue,
+    });
+
+    return [...args];
   } catch (err) {
     throw new Error(`An error occured (${err.message}) when extracting ICU arguments for '${string}'`);
   }


Test patch:
diff --git a/tests-node/unit/broccoli/translation-reducer/lint-test.js b/tests-node/unit/broccoli/translation-reducer/lint-test.js
index b253bcd5..8b485f6b 100644
--- a/tests-node/unit/broccoli/translation-reducer/lint-test.js
+++ b/tests-node/unit/broccoli/translation-reducer/lint-test.js
@@ -148,4 +148,89 @@ describe('linting', function () {
       ['missing', ['en']],
     ]);
   });
+
+  it('detects mismatched arguments', function () {
+    const results = this.linter.lint({
+      de: {
+        key: `{count}`,
+      },
+      en: {
+        key: ``,
+      },
+    });
+
+    expect(results.icuMismatch).to.deep.equal([['key', [['en', ['count']]]]]);
+  });
+
+  it('detects date arguments', function () {
+    const results = this.linter.lint({
+      de: {
+        key: `{timestamp, date, long}`,
+      },
+      en: {
+        key: ``,
+      },
+    });
+
+    expect(results.icuMismatch).to.deep.equal([['key', [['en', ['timestamp']]]]]);
+  });
+
+  it('detects time arguments', function () {
+    const results = this.linter.lint({
+      de: {
+        key: `{timestamp, time, short}`,
+      },
+      en: {
+        key: ``,
+      },
+    });
+
+    expect(results.icuMismatch).to.deep.equal([['key', [['en', ['timestamp']]]]]);
+  });
+
+  it('detects select arguments', function () {
+    const results = this.linter.lint({
+      de: {
+        key: `{gender, select, male {He avoids bugs} female {She avoids bugs} other {They avoid bugs}}`,
+      },
+      en: {
+        key: ``,
+      },
+    });
+
+    expect(results.icuMismatch).to.deep.equal([['key', [['en', ['gender']]]]]);
+  });
+
+  it('detects plural arguments', function () {
+    const results = this.linter.lint({
+      de: {
+        key: `{numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}`,
+      },
+      en: {
+        key: ``,
+      },
+    });
+
+    expect(results.icuMismatch).to.deep.equal([['key', [['en', ['numPhotos']]]]]);
+  });
+
+  it('detects arguments within plural arguments', function () {
+    const results = this.linter.lint({
+      de: {
+        key: `{count, plural,
+        one {Ein Fehler hat die Speicherung von {model} verhindert}
+        other {# Fehler hat verhindert, dass {model} gespeichert wurden}
+      }`,
+      },
+      en: {
+        key: `{count, plural,
+        one {An error}
+        other {# errors}
+      } prevented this {model} from being saved`,
+      },
+    });
+
+    expect(results.icuMismatch).to.deep.equal([]);
+    expect(results.missingTranslations).to.deep.equal([]);
+  });
 });