Refactor options page to use Typescript

Also, I've added and ran eslint, and fixed several minor issues accross
the Typescript codebase.

Bug: translateselectedtext:15
Change-Id: I8cfd67697f9bfb22f6de93b64fd750de66bab863
diff --git a/src/options/elements/credits-dialog/credits-dialog.js b/src/options/elements/credits-dialog/credits-dialog.ts
similarity index 67%
rename from src/options/elements/credits-dialog/credits-dialog.js
rename to src/options/elements/credits-dialog/credits-dialog.ts
index e7c3b10..e9201ee 100644
--- a/src/options/elements/credits-dialog/credits-dialog.js
+++ b/src/options/elements/credits-dialog/credits-dialog.ts
@@ -1,14 +1,15 @@
 import {css, html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators.js';
 import {map} from 'lit/directives/map.js';
 import {unsafeHTML} from 'lit/directives/unsafe-html.js';
 
 import {msg} from '../../../common/i18n';
-import credits from '../../credits.json5';
-import i18nCredits from '../../i18n-credits.json5';
 import {DIALOG_STYLES} from '../../shared/dialog-styles';
 import {SHARED_STYLES} from '../../shared/shared-styles';
+import {credits, i18nCredits} from '../../tsCredits';
 
-export class CreditsDialog extends LitElement {
+@customElement('credits-dialog')
+export default class CreditsDialog extends LitElement {
   static get styles() {
     return [
       SHARED_STYLES,
@@ -64,34 +65,29 @@
   }
 
   render() {
-    let translators = map(i18nCredits, contributor => {
-      let languagesArray =
-          contributor?.languages?.map?.(lang => lang?.name ?? 'undefined');
-      let languages =
+    const translators = map(i18nCredits, (contributor) => {
+      const languagesArray =
+          contributor?.languages?.map?.((lang) => lang?.name ?? 'undefined');
+      const languages =
           languagesArray.length > 0 ? ': ' + languagesArray.join(', ') : '';
       return html`
-        <li>
-          <span class="name">${contributor?.name}</span>${languages}
-        </li>
+        <li><span class="name">${contributor?.name}</span>${languages}</li>
       `;
     });
 
-    let homepageMsg = msg('options_credits_homepage');
-    let creditsByMsg = msg('options_credits_by');
+    const homepageMsg = msg('options_credits_homepage');
+    const creditsByMsg = msg('options_credits_by');
 
-    let otherCredits = map(credits, c => {
-      let url = c.url ? html`
-            <a href=${c?.url} target="_blank" class="homepage">
-              ${homepageMsg}
-            </a>` :
-                        undefined;
-      let license = c.license ? ' - ' + c.license : '';
-      let author = c.author ? html`
-        <p class="author">
-          ${creditsByMsg} ${c.author}${license}
-        </p>
-      ` :
-                              undefined;
+    const otherCredits = map(credits, (c) => {
+      const url = c.url ?
+          html` <a href=${c?.url} target="_blank" class="homepage">
+            ${homepageMsg}
+          </a>` :
+          undefined;
+      const license = c.license ? ' - ' + c.license : '';
+      const author = c.author ?
+          html` <p class="author">${creditsByMsg} ${c.author}${license}</p> ` :
+          undefined;
 
       return html`
         <div class="entry">
@@ -119,9 +115,7 @@
               ${translators}
             </ul>
           </div>
-          <div class="content_area">
-            ${otherCredits}
-          </div>
+          <div class="content_area">${otherCredits}</div>
         </div>
         <div class="action_buttons">
           <button id="credits_ok" @click="${this.closeDialog}">
@@ -133,14 +127,13 @@
   }
 
   showDialog() {
-    let dialog = this.renderRoot.querySelector('dialog');
+    const dialog = this.renderRoot.querySelector('dialog');
     dialog.showModal();
     dialog.querySelector('.scrollable').scrollTo(0, 0);
-    dialog.querySelector('#credits_ok').focus();
+    (dialog.querySelector('#credits_ok') as HTMLElement).focus();
   }
 
   closeDialog() {
     this.renderRoot.querySelector('dialog').close();
   }
 }
-customElements.define('credits-dialog', CreditsDialog);
diff --git a/src/options/elements/options-editor/add-language-dialog.js b/src/options/elements/options-editor/add-language-dialog.ts
similarity index 72%
rename from src/options/elements/options-editor/add-language-dialog.js
rename to src/options/elements/options-editor/add-language-dialog.ts
index 4cfd711..155d4e5 100644
--- a/src/options/elements/options-editor/add-language-dialog.js
+++ b/src/options/elements/options-editor/add-language-dialog.ts
@@ -1,23 +1,30 @@
 import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
 
-import {isoLangs} from '../../../common/consts';
+import {IsoLang, isoLangs} from '../../../common/consts';
 import {msg} from '../../../common/i18n';
+import {TargetLangs} from '../../../common/options';
 import {DIALOG_STYLES} from '../../shared/dialog-styles';
 import {SHARED_STYLES} from '../../shared/shared-styles';
 
+interface IsoLangWCode extends IsoLang {
+  code: string;
+}
+
 const ALL_LANGUAGES =
     Object.entries(isoLangs)
         .map(entry => {
-          let lang = entry[1];
-          lang.code = entry[0];
+          const lang: IsoLangWCode = Object.assign(entry[1], {code: entry[0]});
           return lang;
         })
         .sort((a, b) => a.name < b.name ? -1 : (a.name > b.name ? 1 : 0));
 
-export class AddLanguageDialog extends LitElement {
+@customElement('add-language-dialog')
+export default class AddLanguageDialog extends LitElement {
   static properties = {
     languages: {type: Object},
   };
+  @property({type: Object}) languages: TargetLangs;
 
   static get styles() {
     return [
@@ -51,8 +58,8 @@
   }
 
   render() {
-    let languageCodes = Object.values(this.languages ?? {});
-    let languages = ALL_LANGUAGES
+    const languageCodes = Object.values(this.languages ?? {});
+    const languages = ALL_LANGUAGES
                         .filter(lang => {
                           return !languageCodes.includes(lang.code);
                         })
@@ -88,7 +95,7 @@
   }
 
   showDialog() {
-    let dialog = this.renderRoot.querySelector('dialog');
+    const dialog = this.renderRoot.querySelector('dialog');
     dialog.showModal();
   }
 
@@ -97,16 +104,15 @@
   }
 
   addLanguage() {
-    let languageCodes = Object.values(this.languages ?? {});
-    let select = this.renderRoot.querySelector('#select_language');
+    const languageCodes = Object.values(this.languages ?? {});
+    const select = this.renderRoot.querySelector('#select_language') as HTMLSelectElement;
 
-    let newLang = select.value;
+    const newLang = select.value;
     languageCodes.push(newLang);
-    let translateinto = Object.assign({}, languageCodes);
+    const translateinto = Object.assign({}, languageCodes);
     chrome.storage.sync.set({translateinto}, () => {
       select.selectedIndex = 0;
       this.closeDialog();
     });
   }
 }
-customElements.define('add-language-dialog', AddLanguageDialog);
diff --git a/src/options/elements/options-editor/languages-editor.js b/src/options/elements/options-editor/languages-editor.ts
similarity index 83%
rename from src/options/elements/options-editor/languages-editor.js
rename to src/options/elements/options-editor/languages-editor.ts
index 76d9967..64c3f84 100644
--- a/src/options/elements/options-editor/languages-editor.js
+++ b/src/options/elements/options-editor/languages-editor.ts
@@ -1,16 +1,17 @@
+import './add-language-dialog';
+
 import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
 import {map} from 'lit/directives/map.js';
 
 import {isoLangs} from '../../../common/consts';
 import {msg} from '../../../common/i18n';
+import {TargetLangs} from '../../../common/options';
 import {SHARED_STYLES} from '../../shared/shared-styles';
 
-import AddLanguageDialog from './add-language-dialog';
-
-export class LanguagesEditor extends LitElement {
-  static properties = {
-    languages: {type: Object},
-  };
+@customElement('languages-editor')
+export default class LanguagesEditor extends LitElement {
+  @property({type: Object}) languages: TargetLangs;
 
   static get styles() {
     return [
@@ -84,16 +85,10 @@
     ];
   }
 
-  constructor() {
-    super();
-    this.addEventListener('show-credits-dialog', this.showDialog);
-    this.sortable = undefined;
-  }
-
   render() {
-    let languageCodes = Object.values(this.languages ?? {});
-    let languageList = map(languageCodes, (lang, i) => {
-      let moveBtns = [];
+    const languageCodes = Object.values(this.languages ?? {});
+    const languageList = map(languageCodes, (lang, i) => {
+      const moveBtns = [];
       if (i != 0) {
         moveBtns.push(html`
           <button
@@ -160,29 +155,28 @@
     this.renderRoot.querySelector('add-language-dialog').dispatchEvent(e);
   }
 
-  save(languageCodes) {
-    let translateinto = Object.assign({}, languageCodes);
+  save(languageCodes: string[]) {
+    const translateinto = Object.assign({}, languageCodes);
     chrome.storage.sync.set({translateinto});
   }
 
-  deleteLanguage(deleteLang) {
-    let languageCodes =
+  deleteLanguage(deleteLang: string) {
+    const languageCodes =
         Object.values(this.languages ?? {}).filter(lang => lang != deleteLang);
     this.save(languageCodes);
   }
 
-  swapLanguages(i, j) {
-    let languageCodes = Object.values(this.languages ?? {});
+  swapLanguages(i: number, j: number) {
+    const languageCodes = Object.values(this.languages ?? {});
     if (i >= languageCodes.length || j >= languageCodes.length || i < 0 ||
         j < 0) {
       console.error(
           'Can\'t swap languages because the indexes are out of the range.');
       return;
     }
-    let tmp = languageCodes[j];
+    const tmp = languageCodes[j];
     languageCodes[j] = languageCodes[i];
     languageCodes[i] = tmp;
     this.save(languageCodes);
   }
 }
-customElements.define('languages-editor', LanguagesEditor);
diff --git a/src/options/elements/options-editor/options-editor.js b/src/options/elements/options-editor/options-editor.ts
similarity index 67%
rename from src/options/elements/options-editor/options-editor.js
rename to src/options/elements/options-editor/options-editor.ts
index a26072e..2d0422e 100644
--- a/src/options/elements/options-editor/options-editor.js
+++ b/src/options/elements/options-editor/options-editor.ts
@@ -1,16 +1,16 @@
+import './languages-editor';
+
 import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
 import {map} from 'lit/directives/map.js';
 
 import {msg} from '../../../common/i18n';
-import {TAB_OPTIONS} from '../../../common/options';
+import {OptionsV0, TAB_OPTIONS, TabOptionValue} from '../../../common/options';
 import {SHARED_STYLES} from '../../shared/shared-styles';
 
-import LanguagesEditor from './languages-editor';
-
-export class OptionsEditor extends LitElement {
-  static properties = {
-    storageData: {type: Object},
-  };
+@customElement('options-editor')
+export default class OptionsEditor extends LitElement {
+  @property({type: Object}) storageData: OptionsV0;
 
   static get styles() {
     return [
@@ -24,16 +24,11 @@
     ];
   }
 
-  constructor() {
-    super();
-    this.addEventListener('show-credits-dialog', this.showDialog);
-  }
-
   render() {
-    let currentTabOption = this.storageData?.uniquetab;
+    const currentTabOption = this.storageData?.uniquetab;
 
-    let otherOptions = map(TAB_OPTIONS, (option, i) => {
-      let checked = option.value == currentTabOption ||
+    const otherOptions = map(TAB_OPTIONS, (option, i) => {
+      const checked = option.value == currentTabOption ||
           option.deprecatedValues.includes(currentTabOption);
       return html`
             <p>
@@ -56,10 +51,9 @@
     `;
   }
 
-  changeTabOption(value) {
+  changeTabOption(value: TabOptionValue) {
     chrome.storage.sync.set({uniquetab: value}, function() {
       chrome.runtime.sendMessage({action: 'clearTranslatorTab'});
     });
   }
 }
-customElements.define('options-editor', OptionsEditor);
diff --git a/src/options/options.js b/src/options/options.ts
similarity index 76%
rename from src/options/options.js
rename to src/options/options.ts
index 2dd9dca..6cdcec9 100644
--- a/src/options/options.js
+++ b/src/options/options.ts
@@ -1,17 +1,21 @@
+import './elements/credits-dialog/credits-dialog';
+import './elements/options-editor/options-editor';
+
 import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
 
 import {msg} from '../common/i18n';
-import Options from '../common/options';
+import {default as Options, OptionsV0} from '../common/options';
 
-import CreditsDialog from './elements/credits-dialog/credits-dialog';
-import OptionsEditor from './elements/options-editor/options-editor';
 import {SHARED_STYLES} from './shared/shared-styles';
 
-let bodyStyles = document.createElement('style');
-// #!if browser_target == 'chromium'
-let widthProperty = 'width: 470px;';
-// #!else
-let widthProperty = '';
+const bodyStyles = document.createElement('style');
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore #!if browser_target == 'chromium'
+const widthProperty = 'width: 470px;';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore #!else
+const widthProperty = '';
 // #!endif
 bodyStyles.textContent = `
   body {
@@ -24,16 +28,15 @@
 
 document.head.append(bodyStyles);
 
+@customElement('options-page')
 export class OptionsPage extends LitElement {
-  static properties = {
-    _storageData: {type: Object, state: true},
-  }
+  @property({type: Object, state: true}) _storageData: OptionsV0;
 
   constructor() {
     super();
     this._storageData = undefined;
     this.updateStorageData();
-    chrome.storage.onChanged.addListener((changes, areaName) => {
+    chrome.storage.onChanged.addListener((_changes, areaName) => {
       if (areaName == 'sync') this.updateStorageData();
     });
   }
@@ -105,4 +108,3 @@
     this.renderRoot.querySelector('credits-dialog').dispatchEvent(e);
   }
 }
-customElements.define('options-page', OptionsPage);
diff --git a/src/options/shared/dialog-styles.js b/src/options/shared/dialog-styles.ts
similarity index 100%
rename from src/options/shared/dialog-styles.js
rename to src/options/shared/dialog-styles.ts
diff --git a/src/options/shared/shared-styles.js b/src/options/shared/shared-styles.ts
similarity index 100%
rename from src/options/shared/shared-styles.js
rename to src/options/shared/shared-styles.ts
diff --git a/src/options/tsCredits.ts b/src/options/tsCredits.ts
new file mode 100644
index 0000000..cbbcc4b
--- /dev/null
+++ b/src/options/tsCredits.ts
@@ -0,0 +1,26 @@
+import creditsRaw from './credits.json5';
+import i18nCreditsRaw from './i18n-credits.json5';
+
+interface Credit {
+  name: string;
+  url?: string;
+  author?: string;
+  license?: string;
+}
+
+type Credits = Credit[]
+
+interface CrowdinLang {
+  id: string;
+  name: string;
+}
+
+interface IntCredit {
+  name: string;
+  languages: CrowdinLang[];
+}
+
+type IntCredits = IntCredit[];
+
+export const credits: Credits = creditsRaw;
+export const i18nCredits: IntCredits = i18nCreditsRaw;