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

Issue:
 "Message can not be parsed" warning on specific locale
### Environment

- Ember Version:  3.10.1
- Ember CLI Version: 3.10.1
- Ember Intl Version: 3.5.2

### Steps to Reproduce

I have `ypk-us-ak` locale (stands for [Yupik lang](https://en.wikipedia.org/wiki/Yupik_languages)) in my app.  But for some reason I get warning in build time:

`WARNING: [ember-intl] ypk-us-ak: "dayCount" message can not be parsed: Cannot read property 'indexOf' of undefined`

**NOTE:** it happens only if pluralization is involved. Translation works, despite of warning.

Repo with replication: https://github.com/yratanov/intl-bug

Pull request:
fix: Warns when encountering an unknown language and skips message validation if it is unknown.
fixes #890

Patch:
diff --git a/lib/broccoli/translation-reducer/index.js b/lib/broccoli/translation-reducer/index.js
index 17672c71..76e1d773 100644
--- a/lib/broccoli/translation-reducer/index.js
+++ b/lib/broccoli/translation-reducer/index.js
@@ -18,6 +18,7 @@ const forEachMessage = require('./utils/for-each-message');
 const validateMessage = require('../../message-validator/validate-message');
 const stripNestedNulls = require('./utils/strip-nested-nulls');
 const wrapWithNamespaceIfNeeded = require('./utils/wrap-with-namespace-if-needed');
+const isKnownLanguage = require('./utils/is-known-language');
 const enums = require('../enums');
 
 function readAsObject(filepath) {
@@ -35,7 +36,7 @@ function readAsObject(filepath) {
 
 function normalizeLocale(locale) {
   if (typeof locale === 'string') {
-    return locale.replace(/_/g, '-').toLowerCase();
+    return locale.replace(/_/g, '-').trim().toLowerCase();
   }
 
   return locale;
@@ -227,11 +228,25 @@ class TranslationReducer extends CachingWriter {
   }
 
   validateMessages(messages, locale) {
+    const language = locale.split('-')[0];
+
+    if (!isKnownLanguage(language)) {
+      this._logLocale(
+        locale,
+        `Unable to detect language data for "${language}". Language code is either unknown or invalid.`,
+        {
+          warning: true,
+        }
+      );
+
+      return false;
+    }
+
     forEachMessage(messages, (key, message) => {
       try {
         validateMessage(message, locale);
       } catch (error) {
-        this._logLocale(locale, `"${key}" message can not be parsed: ${error.message}`, { warning: true });
+        this._logLocale(locale, `"${key}" message cannot be parsed: ${error.message}`, { warning: true });
       }
     });
   }
diff --git a/lib/broccoli/translation-reducer/utils/is-known-language.js b/lib/broccoli/translation-reducer/utils/is-known-language.js
new file mode 100644
index 00000000..3f8f6771
--- /dev/null
+++ b/lib/broccoli/translation-reducer/utils/is-known-language.js
@@ -0,0 +1,6 @@
+const ordinals = require('cldr-core/supplemental/ordinals.json').supplemental['plurals-type-ordinal'];
+const plurals = require('cldr-core/supplemental/plurals.json').supplemental['plurals-type-cardinal'];
+
+module.exports = function isKnownLanguage(language) {
+  return language in ordinals || language in plurals;
+};
diff --git a/lib/message-validator/validate-message.js b/lib/message-validator/validate-message.js
index 8ef510b4..4d8fce62 100755
--- a/lib/message-validator/validate-message.js
+++ b/lib/message-validator/validate-message.js
@@ -7,13 +7,10 @@ const traverse = require('./ast-traverse');
 const { TYPE } = messageParser;
 
 function validateMessage(message, locale) {
-  let ast = messageParser.parse(message);
-
-  locale = locale || '';
-
-  let language = locale.split('-')[0];
-  let validPlurals = pluralCategories[language];
-  let validOrdinals = ordinalCategories[language];
+  const ast = messageParser.parse(message);
+  const language = locale.split('-')[0];
+  const validPlurals = pluralCategories[language];
+  const validOrdinals = ordinalCategories[language];
 
   traverse(ast, {
     [TYPE.plural]: (node) => {
@@ -27,9 +24,9 @@ function validateMessage(message, locale) {
         return;
       }
 
-      let isOrdinal = node.pluralType === 'ordinal';
-      let validSelectors = isOrdinal ? validOrdinals : validPlurals;
-      let invalidSelectors = selectors.filter((selector) => {
+      const isOrdinal = node.pluralType === 'ordinal';
+      const validSelectors = isOrdinal ? validOrdinals : validPlurals;
+      const invalidSelectors = selectors.filter((selector) => {
         return validSelectors.indexOf(selector) === -1 && !/=\d+/.test(selector);
       });
 


Test patch:
diff --git a/tests-node/unit/broccoli/translation-reducer/index-test.js b/tests-node/unit/broccoli/translation-reducer/index-test.js
index a226f3fb..1674286d 100644
--- a/tests-node/unit/broccoli/translation-reducer/index-test.js
+++ b/tests-node/unit/broccoli/translation-reducer/index-test.js
@@ -191,9 +191,11 @@ describe('translation-reducer', function () {
       const logs = [];
       const subject = new TranslationReducer(this.input.path(), {
         verbose: true,
+        log(msg) {
+          logs.push(msg);
+        },
       });
 
-      subject._log = (msg) => logs.push(msg);
       subject.handleLintResult(subject.linter.lint(this.fixture));
 
       expect(logs).to.satisfy((string) =>
@@ -208,14 +210,37 @@ describe('translation-reducer', function () {
       );
     });
 
+    it('warns on unknown languages', function () {
+      const logs = [];
+      const subject = new TranslationReducer(this.input.path(), {
+        verbose: true,
+        log(msg) {
+          logs.push(msg);
+        },
+      });
+
+      this.input.write({
+        'en-us.json': `{ "greet": "hi" }`,
+        'ypk-us-ak.json': `{ "greet": "hello" }`,
+      });
+
+      return build(createBuilder(subject), () => {
+        expect(logs).to.have.lengthOf(1);
+        expect(logs).to.deep.equal([
+          `🇦🇰  ypk-us-ak: Unable to detect language data for "ypk". Language code is either unknown or invalid.`,
+        ]);
+      });
+    });
+
     it('handleLintResult throws if errorOnMissingTranslations is set', function () {
       const logs = [];
       const subject = new TranslationReducer(this.input.path(), {
         errorOnMissingTranslations: true,
+        log(msg) {
+          logs.push(msg);
+        },
       });
 
-      subject._log = (msg) => logs.push(msg);
-
       expect(() => subject.handleLintResult(subject.linter.lint(this.fixture))).throws(`Missing translations:
 - "foo" was not found in "it"
 - "bar" was not found in "en", "fr", "it"
@@ -229,10 +254,11 @@ describe('translation-reducer', function () {
       const logs = [];
       const subject = new TranslationReducer(this.input.path(), {
         errorOnNamedArgumentMismatch: true,
+        log(msg) {
+          logs.push(msg);
+        },
       });
 
-      subject._log = (msg) => logs.push(msg);
-
       expect(() => subject.handleLintResult(subject.linter.lint(this.icuFixture))).throws(`ICU arguments mismatch:
 - "foo" ICU argument mismatch: "de": "whos", "en": "who"
 - "missingArg" ICU argument mismatch: "en": "numPhotos"
diff --git a/tests-node/unit/message-validator/validate-message-test.js b/tests-node/unit/message-validator/validate-message-test.js
index 34d4c932..f544c25c 100644
--- a/tests-node/unit/message-validator/validate-message-test.js
+++ b/tests-node/unit/message-validator/validate-message-test.js
@@ -94,7 +94,7 @@ describe('validateMessage', function () {
 
   missingOther.forEach((message) => {
     it(`throws missing other error for "${message}"`, function () {
-      expect(() => validateMessage(message)).to.throw('Missing selector: other');
+      expect(() => validateMessage(message, 'en-us')).to.throw('Missing selector: other');
     });
   });
 });