Refactor background page to use Typescript

Also, this CL adds clean-terminal-webpack-plugin to make it easier to
debug Typescript errors while developing the extension.

Bug: translateselectedtext:15
Change-Id: If9b97cb7508859e2e05f5dc82940808fd935bf1a
diff --git a/src/common/options.ts b/src/common/options.ts
new file mode 100644
index 0000000..6268f22
--- /dev/null
+++ b/src/common/options.ts
@@ -0,0 +1,210 @@
+import {convertLanguages, isoLangs} from './consts';
+
+type TabOptionValue = ''|'yep'|'popup';
+type DeprecatedTabOptionValue = 'panel';
+type TabOptionValueIncludingDeprecated =
+    TabOptionValue|DeprecatedTabOptionValue;
+
+interface TabOption {
+  value: TabOptionValue;
+  labelMsg: string;
+  deprecatedValues: TabOptionValueIncludingDeprecated[];
+}
+
+interface TargetLangs {
+  [key: string]: string;  // Here the key is a string with a number.
+}
+interface OptionsV0 {
+  translateinto: TargetLangs;
+  uniquetab: TabOptionValue;
+}
+
+interface LegacyLanguages {
+  [key: string]: string;  // Here the key is a string with the language code.
+}
+/**
+ * Backwards-compatible interface for the information available in the sync
+ * storage area.
+ */
+interface LegacyOptions {
+  translateinto: TargetLangs;
+  languages: LegacyLanguages;
+  uniquetab: TabOptionValueIncludingDeprecated;
+}
+
+interface OptionsWrapper {
+  options: OptionsV0;
+  isFirstRun: boolean;
+}
+
+export const TAB_OPTIONS: TabOption[] = [
+  // Open in new tab for each translation
+  {
+    value: '',
+    labelMsg: 'options_tabsoption_1',
+    deprecatedValues: [],
+  },
+  // Open in a unique tab
+  {
+    value: 'yep',
+    labelMsg: 'options_tabsoption_2',
+    deprecatedValues: [],
+  },
+  // Open in a popup
+  {
+    value: 'popup',
+    labelMsg: 'options_tabsoption_3',
+    deprecatedValues: ['panel'],
+  },
+];
+
+// Class which can be used to retrieve the user options in order to act
+// accordingly.
+export default class Options {
+  _options: OptionsV0;
+  isFirstRun: boolean;
+
+  constructor(options: OptionsV0, isFirstRun: boolean) {
+    this._options = options;
+    this.isFirstRun = isFirstRun;
+  }
+
+  get uniqueTab(): TabOptionValue {
+    return this._options.uniquetab;
+  }
+
+  get targetLangs(): TargetLangs {
+    return this._options.translateinto;
+  }
+
+  // Returns a promise that resolves in an instance of the Object class with the
+  // current options.
+  static getOptions(readOnly: boolean = true): Promise<Options> {
+    return Options.getOptionsRaw(readOnly).then(res => {
+      return new Options(res.options, res.isFirstRun);
+    });
+  }
+
+  // Returns a promise that resolves to an object containing:
+  // - |options|: normalized options object which can be used to initialize the
+  // Options class, and which contains the current options set up by the user.
+  // - |isFirstRun|: whether the extension is running for the first time and
+  // needs to be set up.
+  //
+  // If the options needed to be normalized/created, they are also saved in the
+  // sync storage area.
+  static getOptionsRaw(readOnly: boolean): Promise<OptionsWrapper> {
+    return new Promise((res, rej) => {
+      chrome.storage.sync.get(null, itemsAny => {
+        if (chrome.runtime.lastError) {
+          return rej(chrome.runtime.lastError);
+        }
+
+        let items = <LegacyOptions>itemsAny;
+        let didTranslateintoChange = false;
+        let didUniquetabChange = false;
+
+        // If the extension sync storage area is blank, set this as being the
+        // first run.
+        let isFirstRun = Object.keys(items).length === 0;
+
+        // Create |translateinto| property if it doesn't exist.
+        if (items.translateinto === undefined) {
+          didTranslateintoChange = true;
+
+          // Upgrade from a version previous to v0.7 if applicable, otherwise
+          // create the property with the default values.
+          if (items.languages !== undefined) {
+            let newTranslateinto: TargetLangs;
+            items.translateinto =
+                Object.assign(newTranslateinto, Object.values(items.languages));
+          } else {
+            let uiLocale = chrome.i18n.getMessage('@@ui_locale');
+            let defaultLang1 = uiLocale.replace('_', '-');
+            let defaultLang2 = uiLocale.split('_')[0];
+
+            items.translateinto = {};
+            if (isoLangs[defaultLang1] != undefined)
+              items.translateinto['0'] = defaultLang1;
+            else if (isoLangs[defaultLang2] != undefined)
+              items.translateinto['0'] = defaultLang2;
+          }
+        }
+
+        // Normalize |translateinto| property: remove non-existent languages or
+        // change them with the correct language code.
+        for (let [index, language] of Object.entries(items.translateinto)) {
+          if (isoLangs[language] === undefined) {
+            didTranslateintoChange = true;
+            if (convertLanguages[language] === undefined) {
+              // The language doesn't exist
+              console.log(
+                  'Deleting ' + language +
+                  ' from items.translateinto because it doesn\'t exist.');
+              delete items.translateinto[index];
+            } else {
+              // The language doesn't exist but a known replacement is known
+              let newLanguage = convertLanguages[language];
+              console.log('Replacing ' + language + ' with ' + newLanguage);
+
+              // If the converted language is already on the list, just remove
+              // the wrong language, otherwise convert the language
+              if (Object.values(items.translateinto).includes(newLanguage))
+                delete items.translateinto[index];
+              else
+                items.translateinto[index] = newLanguage;
+            }
+          }
+        }
+
+        // Normalize |uniquetab| property:
+        // - If it is set to a valid value, leave it alone.
+        // - If it is set to a deprecated value, change it to the corresponding
+        // value we use now.
+        // - If it is set to an incorrect value or it isn't set, change it to
+        // the default value.
+        let uniquetabNewValue: TabOptionValue;
+        let foundValue = false;
+        for (let opt of TAB_OPTIONS) {
+          if (opt.value == items?.uniquetab) {
+            uniquetabNewValue = opt.value;
+            foundValue = true;
+            break;
+          }
+          if (opt.deprecatedValues.includes(items?.uniquetab)) {
+            foundValue = true;
+            uniquetabNewValue = opt.value;
+            break;
+          }
+        }
+        if (!foundValue) {
+          uniquetabNewValue = 'popup';
+          didUniquetabChange = true;
+        }
+
+        // Clean up deprecated properties
+        if (items.languages !== undefined) {
+          delete items.languages;
+          chrome.storage.sync.remove('languages');
+        }
+
+        let returnObject: OptionsWrapper = {
+          isFirstRun,
+          options: {
+            translateinto: items.translateinto,
+            uniquetab: uniquetabNewValue,
+          }
+        };
+
+        // Save properties that have changed if we're not in read-only mode
+        if (!readOnly) {
+          if (didTranslateintoChange || didUniquetabChange) {
+            chrome.storage.sync.set(returnObject.options);
+          }
+        }
+
+        res(returnObject);
+      });
+    });
+  }
+}