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

Issue:
Application translations DOES NOT override addon translations
- [x] I am on the [latest](https://github.com/ember-intl/ember-intl/releases/latest) ember-intl version
- [x] I have searched the [issues](https://github.com/ember-intl/ember-intl/issues) of this repo and believe that this is not a duplicate

### Environment

- Ember Version: 3.12.1
- Ember CLI Version: 3.12.0
- Ember Intl Version: 4.2.0
- Browser(s): Chrome
- Node Version: v10.16.3

### Steps to Reproduce

- Create an empty app, add `ember-intl` add-on
- Create 2 empty add-on, called "aaaa" & the other "zzzz"
- Add `ember-intl` in the 2 add-ons
- Create a translation called "aaaa_translation" in `translations/en-us.yaml` of "aaaa" add-on
- Create a translation called "zzzz_translation" in `translations/en-us.yaml` of "zzzz" add-on
- `yarn/npm link` the add-ons with the app & add it in `package.json` of the application
- Override "aaaa_translation" & "zzzz_translation" in in `translations/en-us.yaml` of the application
- `ember serve` ...

![image](https://user-images.githubusercontent.com/13900970/69610609-740f8100-102c-11ea-9598-75afe9d1091f.png)


It seems that the translations files are not correctly merged. Here a dump of `listFiles` from the `reduce` function in `lib/broccoli/translation-reducer/index.js` :
```
[ '/tmp/broccoli-54539WABk1c5bWG3/out-229-funnel/aaaa/en-us.yaml',
  '/tmp/broccoli-54539WABk1c5bWG3/out-229-funnel/en-us.yaml',
  '/tmp/broccoli-54539WABk1c5bWG3/out-229-funnel/fr-fr.yaml',
  '/tmp/broccoli-54539WABk1c5bWG3/out-229-funnel/zzzz/en-us.yaml' ]
```

`en-us.yaml` from "zzzz" add-on always override the `en-us.yaml` from application.

Pull request:
fix: app should beat addon translations when there is a translation key collision
Fixes #1058 

Patch:
diff --git a/.gitignore b/.gitignore
index d2b0dc59..64efc91c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ npm-debug.log*
 yarn-error.log
 testem.log
 jsconfig.json
+/.vscode
 
 # ember-try
 .node_modules.ember-try/
diff --git a/index.js b/index.js
index 5a3f876b..0c62710f 100644
--- a/index.js
+++ b/index.js
@@ -15,7 +15,7 @@ const stringify = require('json-stable-stringify');
 const extract = require('@ember-intl/broccoli-cldr-data');
 const calculateCacheKeyForTree = require('calculate-cache-key-for-tree');
 
-const buildTree = require('./lib/broccoli/build-translation-tree');
+const buildTranslationTree = require('./lib/broccoli/build-translation-tree');
 const TranslationReducer = require('./lib/broccoli/translation-reducer');
 const isValidLocaleFormat = require('./lib/utils/is-valid-locale-format');
 const findEngine = require('./lib/utils/find-engine');
@@ -28,6 +28,7 @@ const DEFAULT_CONFIG = {
   outputPath: 'translations',
   errorOnMissingTranslations: false,
   errorOnNamedArgumentMismatch: false,
+  wrapTranslationsWithNamespace: false,
   requiresTranslation(/* key, locale */) {
     return true;
   }
@@ -65,7 +66,12 @@ module.exports = {
   },
 
   generateTranslationTree(bundlerOptions) {
-    const [translationTree, addons] = buildTree(this.project, this.opts.inputPath, this.treeGenerator);
+    const [translationTree, addonsWithTranslations] = buildTranslationTree(
+      this.project,
+      this.opts.inputPath,
+      this.treeGenerator
+    );
+
     const _bundlerOptions = bundlerOptions || {};
     const addon = this;
 
@@ -75,7 +81,7 @@ module.exports = {
       });
     }
 
-    return new TranslationReducer(translationTree, {
+    return new TranslationReducer([translationTree], {
       verbose: !this.isSilent,
       outputPath: this.opts.outputPath,
       fallbackLocale: this.opts.fallbackLocale,
@@ -86,7 +92,7 @@ module.exports = {
       errorOnNamedArgumentMismatch: this.opts.errorOnNamedArgumentMismatch,
       stripEmptyTranslations: this.opts.stripEmptyTranslations,
       wrapTranslationsWithNamespace: this.opts.wrapTranslationsWithNamespace,
-      addonsWithTranslations: addons,
+      addonsWithTranslations: addonsWithTranslations,
       log() {
         return addon.log.apply(addon, arguments);
       }
diff --git a/lib/broccoli/build-translation-tree.js b/lib/broccoli/build-translation-tree.js
index bf591837..72055477 100644
--- a/lib/broccoli/build-translation-tree.js
+++ b/lib/broccoli/build-translation-tree.js
@@ -8,36 +8,43 @@ const funnel = require('broccoli-funnel');
 const mergeTrees = require('broccoli-merge-trees');
 const WatchedDir = require('broccoli-source').WatchedDir;
 
+const enums = require('./enums');
+
+function funnelTranslations(tree, destDir) {
+  if (!tree) {
+    return;
+  }
+
+  return funnel(tree, {
+    destDir: destDir || '.',
+    include: ['**/*.yaml', '**/*.yml', '**/*.json']
+  });
+}
+
 function buildTranslationTree(project, inputPath, treeGenerator) {
   const projectTranslations = path.join(project.root, inputPath);
+  const addonTrees = [];
+  const addonsWithTranslations = [];
   const trees = [];
-  const addonNames = [];
 
-  processAddons(project.addons, addonNames, trees, treeGenerator);
+  processAddons(project.addons, addonsWithTranslations, addonTrees, treeGenerator);
+
+  if (addonTrees.length > 0) {
+    trees.push(funnelTranslations(mergeTrees(addonTrees, { overwrite: true }), enums.addonNamespace));
+  }
 
   if (fs.existsSync(projectTranslations)) {
     trees.push(new WatchedDir(projectTranslations));
   }
 
-  return [
-    funnel(
-      mergeTrees(trees, {
-        overwrite: true
-      }),
-      {
-        include: ['**/*.yaml', '**/*.yml', '**/*.json']
-      }
-    ),
-
-    addonNames
-  ];
+  return [mergeTrees(trees, { overwrite: true }), addonsWithTranslations];
 }
 
-function processAddons(addons, addonNames, translationTrees, treeGenerator) {
-  addons.forEach(addon => _processAddon(addon, addonNames, translationTrees, treeGenerator));
+function processAddons(addons, addonsWithTranslations, translationTrees, treeGenerator) {
+  addons.forEach(addon => _processAddon(addon, addonsWithTranslations, translationTrees, treeGenerator));
 }
 
-function _processAddon(addon, addonNames, translationTrees, treeGenerator) {
+function _processAddon(addon, addonsWithTranslations, translationTrees, treeGenerator) {
   const addonTranslationPath = path.join(addon.root, 'translations');
   let addonGeneratedTree;
 
@@ -49,15 +56,15 @@ function _processAddon(addon, addonNames, translationTrees, treeGenerator) {
     let additionalTranslationTree = addon.treeForTranslations(addonGeneratedTree);
 
     if (additionalTranslationTree) {
-      addonNames.push(addon.name);
+      addonsWithTranslations.push(addon.name);
       translationTrees.push(funnel(additionalTranslationTree, { destDir: addon.name }));
     }
   } else if (addonGeneratedTree !== undefined) {
-    addonNames.push(addon.name);
+    addonsWithTranslations.push(addon.name);
     translationTrees.push(funnel(addonGeneratedTree, { destDir: addon.name }));
   }
 
-  processAddons(addon.addons, addonNames, translationTrees, treeGenerator);
+  processAddons(addon.addons, addonsWithTranslations, translationTrees, treeGenerator);
 }
 
 module.exports = buildTranslationTree;
diff --git a/lib/broccoli/enums.js b/lib/broccoli/enums.js
new file mode 100644
index 00000000..a3aa246a
--- /dev/null
+++ b/lib/broccoli/enums.js
@@ -0,0 +1,7 @@
+/* eslint-env node */
+
+'use strict';
+
+module.exports = {
+  addonNamespace: '__ember-intl-addon__'
+};
diff --git a/lib/broccoli/translation-reducer/index.js b/lib/broccoli/translation-reducer/index.js
index 047e59c4..8330eaee 100644
--- a/lib/broccoli/translation-reducer/index.js
+++ b/lib/broccoli/translation-reducer/index.js
@@ -25,6 +25,8 @@ const findMissingTranslations = require('./utils/find-missing-translations');
 const findMissingICUArguments = require('./utils/find-missing-icu-arguments');
 const wrapWithNamespaceIfNeeded = require('./utils/wrap-with-namespace-if-needed');
 
+const enums = require('../enums');
+
 function readAsObject(filepath) {
   let data = fs.readFileSync(filepath);
   let ext = path.extname(filepath);
@@ -154,7 +156,22 @@ class TranslationReducer extends CachingWriter {
   }
 
   reduce(listFiles) {
-    return listFiles.reduce((accum, filepath) => {
+    const addonPrefix = `/${enums.addonNamespace}/`;
+    const orderedTranslations = listFiles.sort(function(fileA, fileB) {
+      // Orders all the addons at the top and the application last.
+      // This way the application wins if there is a translation conflict.
+      if (fileA.includes(addonPrefix) && !fileB.includes(addonPrefix)) {
+        return -1;
+      }
+
+      if (!fileA.includes(addonPrefix) && fileB.includes(addonPrefix)) {
+        return 1;
+      }
+
+      return 0;
+    });
+
+    return orderedTranslations.reduce((accum, filepath) => {
       if (fs.statSync(filepath).isDirectory()) {
         return accum;
       }
diff --git a/lib/broccoli/translation-reducer/utils/wrap-with-namespace-if-needed.js b/lib/broccoli/translation-reducer/utils/wrap-with-namespace-if-needed.js
index 44a3c466..d233359d 100644
--- a/lib/broccoli/translation-reducer/utils/wrap-with-namespace-if-needed.js
+++ b/lib/broccoli/translation-reducer/utils/wrap-with-namespace-if-needed.js
@@ -3,6 +3,7 @@
 'use strict';
 
 const path = require('path');
+const enums = require('../../enums');
 
 /**
  * Wraps the root object with a namespace if the file is under subdirectories
@@ -25,18 +26,18 @@ function wrapWithNamespaceIfNeeded(object, filepath, inputPath, addonNames) {
     let prefix = path.sep;
 
     for (let addon of addonNames) {
-      addon = path.sep + addon;
+      let addonPrefix = `${path.sep}${enums.addonNamespace}${path.sep}${addon}`;
 
-      if (dirname.startsWith(addon)) {
-        prefix = addon;
+      if (dirname.startsWith(addonPrefix)) {
+        prefix = addonPrefix;
         break;
       }
     }
 
-    dirname = dirname.replace(prefix, '');
     const dirnameParts = dirname
+      .replace(prefix, '')
       .split(path.sep)
-      .filter(part => part)
+      .filter(part => Boolean(part))
       .reverse();
 
     if (dirnameParts.length > 0) {


Test patch:
diff --git a/tests-node/unit/broccoli/translation-reducer/wrap-with-namespace-if-needed.js b/tests-node/unit/broccoli/translation-reducer/wrap-with-namespace-if-needed.js
index 9490cfb3..d3575d40 100644
--- a/tests-node/unit/broccoli/translation-reducer/wrap-with-namespace-if-needed.js
+++ b/tests-node/unit/broccoli/translation-reducer/wrap-with-namespace-if-needed.js
@@ -1,9 +1,10 @@
 /* eslint-env node */
 'use strict';
 
-let expect = require('chai').expect;
+const expect = require('chai').expect;
 
-let wrapWithNamespaceIfNeeded = require('../../../../lib/broccoli/translation-reducer/utils/wrap-with-namespace-if-needed');
+const wrapWithNamespaceIfNeeded = require('../../../../lib/broccoli/translation-reducer/utils/wrap-with-namespace-if-needed');
+const enums = require('../../../../lib/broccoli/enums');
 
 describe('wrapWithNamespaceIfNeeded', function() {
   [
@@ -17,14 +18,14 @@ describe('wrapWithNamespaceIfNeeded', function() {
     [
       { a: true, b: false },
       { a: true, b: false },
-      '/tmp/broccoli_debug-output_path-l4iBcmcT/an-addon/en-us.json',
+      `/tmp/broccoli_debug-output_path-l4iBcmcT/${enums.addonNamespace}/an-addon/en-us.json`,
       '/tmp/broccoli_debug-output_path-l4iBcmcT',
       ['an-addon', 'other-addon']
     ],
     [
       { a: true, b: false },
       { a: true, b: false },
-      '/tmp/broccoli_debug-output_path-l4iBcmcT/@a-scoped/addon/en-us.json',
+      `/tmp/broccoli_debug-output_path-l4iBcmcT/${enums.addonNamespace}/@a-scoped/addon/en-us.json`,
       '/tmp/broccoli_debug-output_path-l4iBcmcT',
       ['@a-scoped/addon', 'an-addon', 'other-addon']
     ],
@@ -52,42 +53,42 @@ describe('wrapWithNamespaceIfNeeded', function() {
     [
       { a: true, b: false },
       { foo: { bar: { a: true, b: false } } },
-      '/tmp/broccoli_debug-output_path-l4iBcmcT/an-addon/foo/bar/en-us.json',
+      `/tmp/broccoli_debug-output_path-l4iBcmcT/${enums.addonNamespace}/an-addon/foo/bar/en-us.json`,
       '/tmp/broccoli_debug-output_path-l4iBcmcT',
       ['an-addon', 'other-addon']
     ],
     [
       { a: true, b: false },
       { foo: { bar: { a: true, b: false } } },
-      '/tmp/broccoli_debug-output_path-l4iBcmcT/an-addon/foo/bar/en-us.json',
+      `/tmp/broccoli_debug-output_path-l4iBcmcT/${enums.addonNamespace}/an-addon/foo/bar/en-us.json`,
       '/tmp/broccoli_debug-output_path-l4iBcmcT',
       ['other-addon', 'an-addon']
     ],
     [
       { a: true, b: false },
       { foo: { bar: { a: true, b: false } } },
-      '/tmp/broccoli_debug-output_path-l4iBcmcT/@a-scoped/addon/foo/bar/en-us.json',
+      `/tmp/broccoli_debug-output_path-l4iBcmcT/${enums.addonNamespace}/@a-scoped/addon/foo/bar/en-us.json`,
       '/tmp/broccoli_debug-output_path-l4iBcmcT',
       ['@a-scoped/addon', 'other-addon', 'an-addon']
     ],
     [
       { a: true, b: false },
       { foo_bar: { a: true, b: false } },
-      '/tmp/broccoli_debug-output_path-l4iBcmcT/@a-scoped/addon/foo_bar/en-us.json',
+      `/tmp/broccoli_debug-output_path-l4iBcmcT/${enums.addonNamespace}/@a-scoped/addon/foo_bar/en-us.json`,
       '/tmp/broccoli_debug-output_path-l4iBcmcT',
       ['@a-scoped/addon', 'other-addon', 'an-addon']
     ],
     [
       { a: true, b: false },
       { 'foo-bar': { a: true, b: false } },
-      '/tmp/broccoli_debug-output_path-l4iBcmcT/@a-scoped/addon/foo-bar/en-us.json',
+      `/tmp/broccoli_debug-output_path-l4iBcmcT/${enums.addonNamespace}/@a-scoped/addon/foo-bar/en-us.json`,
       '/tmp/broccoli_debug-output_path-l4iBcmcT',
       ['@a-scoped/addon', 'other-addon', 'an-addon']
     ],
     [
       { a: true, b: false },
       { foo_bar: { a: true, b: false } },
-      '/tmp/broccoli_debug-output_path-l4iBcmcT/@a-scoped/addon/foo bar/en-us.json',
+      `/tmp/broccoli_debug-output_path-l4iBcmcT/${enums.addonNamespace}/@a-scoped/addon/foo bar/en-us.json`,
       '/tmp/broccoli_debug-output_path-l4iBcmcT',
       ['@a-scoped/addon', 'other-addon', 'an-addon']
     ]