diff --git a/config.xml b/config.xml
index 71d282dcc..a8c1baeca 100644
--- a/config.xml
+++ b/config.xml
@@ -116,10 +116,10 @@
     
         
     
-    
     
     
     
     
     
+    
 
diff --git a/package-lock.json b/package-lock.json
index 33acaa63c..b963b9677 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54,11 +54,21 @@
       "resolved": "https://registry.npmjs.org/@angular/tsc-wrapped/-/tsc-wrapped-4.4.3.tgz",
       "integrity": "sha1-LT84IQodTbA/yG3PHglYErhc0Rk="
     },
+    "@ionic-native/clipboard": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/@ionic-native/clipboard/-/clipboard-4.3.2.tgz",
+      "integrity": "sha512-dutKKMnpyhFeEPc09O5MzrtPKtv13f3X4mCmDmUEj5CExMecTOhox/rnIB70t8GP2/3bTC/CSr39DhkLLS8MzA=="
+    },
     "@ionic-native/core": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-4.3.0.tgz",
       "integrity": "sha512-Pf0qCzqlVFmIpZpvo35Kl0e+1K8GUgPMcKBnN57gWh+5Ecj3dPcb+MbP4murJo/dnFsIYPYdXRZRf74hjo6gtw=="
     },
+    "@ionic-native/globalization": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/@ionic-native/globalization/-/globalization-4.3.2.tgz",
+      "integrity": "sha512-sDyriA3/xspu6RM8arEOlhOkSxvRwLDNdvbBoZ59i+Dn5i6bjoE4hMMEFvUlnzvOZKn5GusurrWPz5cmfA9aPw=="
+    },
     "@ionic-native/keyboard": {
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/@ionic-native/keyboard/-/keyboard-4.3.2.tgz",
@@ -110,6 +120,11 @@
       "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
       "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ="
     },
+    "@types/cordova-plugin-globalization": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/@types/cordova-plugin-globalization/-/cordova-plugin-globalization-0.0.3.tgz",
+      "integrity": "sha1-ySA6HENtPS0DBXiffJwrq6i6KK0="
+    },
     "@types/cordova-plugin-network-information": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/@types/cordova-plugin-network-information/-/cordova-plugin-network-information-0.0.3.tgz",
@@ -120,6 +135,11 @@
       "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.30.tgz",
       "integrity": "sha1-PWCmv23aOOP4pGlhFZg3nx9klQk="
     },
+    "@types/node": {
+      "version": "8.0.47",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.47.tgz",
+      "integrity": "sha512-kOwL746WVvt/9Phf6/JgX/bsGQvbrK5iUgzyfwZNcKVFcjAUVSpF9HxevLTld2SG9aywYHOILj38arDdY1r/iQ=="
+    },
     "@types/promise.prototype.finally": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/@types/promise.prototype.finally/-/promise.prototype.finally-2.0.2.tgz",
diff --git a/package.json b/package.json
index 89e9c0cf8..34f09290a 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,9 @@
     "@angular/http": "4.4.3",
     "@angular/platform-browser": "4.4.3",
     "@angular/platform-browser-dynamic": "4.4.3",
+    "@ionic-native/clipboard": "^4.3.2",
     "@ionic-native/core": "4.3.0",
+    "@ionic-native/globalization": "^4.3.2",
     "@ionic-native/keyboard": "^4.3.2",
     "@ionic-native/network": "^4.3.2",
     "@ionic-native/splash-screen": "4.3.0",
@@ -43,7 +45,9 @@
     "@ngx-translate/core": "^8.0.0",
     "@ngx-translate/http-loader": "^2.0.0",
     "@types/cordova": "0.0.34",
+    "@types/cordova-plugin-globalization": "0.0.3",
     "@types/cordova-plugin-network-information": "0.0.3",
+    "@types/node": "^8.0.47",
     "@types/promise.prototype.finally": "^2.0.2",
     "electron-builder-squirrel-windows": "^19.3.0",
     "electron-windows-notifications": "^1.1.13",
@@ -58,5 +62,8 @@
   "devDependencies": {
     "@ionic/app-scripts": "3.0.0",
     "typescript": "2.3.4"
+  },
+  "browser": {
+    "electron": false
   }
 }
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 0d832d81c..e1be5a289 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,13 +1,12 @@
 import { BrowserModule } from '@angular/platform-browser';
 import { ErrorHandler, NgModule } from '@angular/core';
 import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
 
 import { SplashScreen } from '@ionic-native/splash-screen';
 import { StatusBar } from '@ionic-native/status-bar';
 import { SQLite } from '@ionic-native/sqlite';
 import { Keyboard } from '@ionic-native/keyboard';
-import { Network } from '@ionic-native/network';
 import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
 import { TranslateHttpLoader } from '@ngx-translate/http-loader';
 
@@ -16,6 +15,7 @@ import { CoreLoggerProvider } from '../providers/logger';
 import { CoreDbProvider } from '../providers/db';
 import { CoreAppProvider } from '../providers/app';
 import { CoreConfigProvider } from '../providers/config';
+import { CoreEmulatorModule } from '../core/emulator/emulator.module';
 
 // For translate loader. AoT requires an exported function for factories.
 export function createTranslateLoader(http: HttpClient) {
@@ -28,6 +28,7 @@ export function createTranslateLoader(http: HttpClient) {
     ],
     imports: [
         BrowserModule,
+        HttpClientModule,
         IonicModule.forRoot(MyApp),
         TranslateModule.forRoot({
             loader: {
@@ -35,7 +36,8 @@ export function createTranslateLoader(http: HttpClient) {
                 useFactory: (createTranslateLoader),
                 deps: [HttpClient]
             }
-        })
+        }),
+        CoreEmulatorModule
     ],
     bootstrap: [IonicApp],
     entryComponents: [
@@ -46,12 +48,11 @@ export function createTranslateLoader(http: HttpClient) {
         SplashScreen,
         SQLite,
         Keyboard,
-        Network,
         {provide: ErrorHandler, useClass: IonicErrorHandler},
         CoreLoggerProvider,
         CoreDbProvider,
         CoreAppProvider,
-        CoreConfigProvider,
+        CoreConfigProvider
     ]
 })
 export class AppModule {}
diff --git a/src/core/emulator/classes/sqlitedb.ts b/src/core/emulator/classes/sqlitedb.ts
new file mode 100644
index 000000000..34210a38f
--- /dev/null
+++ b/src/core/emulator/classes/sqlitedb.ts
@@ -0,0 +1,153 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { SQLiteDB } from '../../../classes/sqlitedb';
+
+/**
+ * Class to mock the interaction with the SQLite database.
+ */
+export class SQLiteDBMock extends SQLiteDB {
+    promise: Promise;
+
+    /**
+     * Create and open the database.
+     *
+     * @param {string} name Database name.
+     */
+    constructor(public name: string) {
+        super(name, null, null);
+    }
+
+    /**
+     * Close the database.
+     *
+     * @return {Promise} Promise resolved when done.
+     */
+    close() : Promise {
+        // WebSQL databases aren't closed.
+        return Promise.resolve();
+    }
+
+    /**
+     * Drop all the data in the database.
+     *
+     * @return {Promise} Promise resolved when done.
+     */
+    emptyDatabase() : Promise {
+        return new Promise((resolve, reject) => {
+            this.db.transaction(tx => {
+                // Query all tables from sqlite_master that we have created and can modify.
+                let args = [],
+                    query = `SELECT * FROM sqlite_master
+                            WHERE name NOT LIKE 'sqlite\\_%' escape '\\' AND name NOT LIKE '\\_%' escape '\\'`;
+
+                tx.executeSql(query, args, (tx, result) => {
+                    if (result.rows.length <= 0) {
+                        // No tables to delete, stop.
+                        resolve();
+                        return;
+                    }
+
+                    // Drop all the tables.
+                    let promises = [];
+
+                    for (let i = 0; i < result.rows.length; i++) {
+                        promises.push(new Promise((resolve, reject) => {
+                            // Drop the table.
+                            let name = JSON.stringify(result.rows.item(i).name);
+                            tx.executeSql('DROP TABLE ' + name, [], resolve, reject);
+                        }));
+                    }
+
+                    Promise.all(promises).then(resolve, reject);
+                }, reject);
+            });
+        });
+    }
+
+    /**
+     * Execute a SQL query.
+     *
+     * @param {string} sql SQL query to execute.
+     * @param {any[]} params Query parameters.
+     * @return {Promise} Promise resolved with the result.
+     */
+    protected execute(sql: string, params?: any[]) : Promise {
+        return new Promise((resolve, reject) => {
+            // With WebSQL, all queries must be run in a transaction.
+            this.db.transaction((tx) => {
+                tx.executeSql(sql, params, (tx, results) => {
+                    resolve(results);
+                }, reject);
+            });
+        });
+    }
+
+    /**
+     * Execute a set of SQL queries. This operation is atomic.
+     *
+     * @param {any[]} sqlStatements SQL statements to execute.
+     * @return {Promise} Promise resolved with the result.
+     */
+    protected executeBatch(sqlStatements: any[]) : Promise {
+        return new Promise((resolve, reject) => {
+            // Create a transaction to execute the queries.
+            this.db.transaction((tx) => {
+                let promises = [];
+
+                // Execute all the queries. Each statement can be a string or an array.
+                sqlStatements.forEach((statement) => {
+                    promises.push(new Promise((resolve, reject) => {
+                        let query,
+                            params;
+
+                        if (Array.isArray(statement)) {
+                            query = statement[0];
+                            params = statement[1];
+                        } else {
+                            query = statement;
+                            params = null;
+                        }
+
+                        tx.executeSql(query, params, (tx, results) => {
+                            resolve(results);
+                        }, reject);
+                    }));
+                });
+
+                Promise.all(promises).then(resolve, reject);
+            });
+        });
+    }
+
+    /**
+     * Initialize the database.
+     */
+    init(): void {
+        // This DB is for desktop apps, so use a big size to be sure it isn't filled.
+        this.db = (window).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024);
+        this.promise = Promise.resolve();
+    }
+
+    /**
+     * Open the database. Only needed if it was closed before, a database is automatically opened when created.
+     *
+     * @return {Promise} Promise resolved when done.
+     */
+    open() : Promise {
+        // WebSQL databases can't closed, so the open method isn't needed.
+        return Promise.resolve();
+    }
+
+}
diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts
new file mode 100644
index 000000000..4c87604d0
--- /dev/null
+++ b/src/core/emulator/emulator.module.ts
@@ -0,0 +1,69 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { Platform } from 'ionic-angular';
+
+import { CoreAppProvider } from '../../providers/app';
+import { Clipboard } from '@ionic-native/clipboard';
+import { Globalization } from '@ionic-native/globalization';
+import { Network } from '@ionic-native/network';
+import { ClipboardMock } from './providers/clipboard';
+import { GlobalizationMock } from './providers/globalization';
+import { NetworkMock } from './providers/network';
+
+@NgModule({
+    declarations: [
+    ],
+    imports: [
+    ],
+    providers: [
+        ClipboardMock,
+        GlobalizationMock,
+        {
+            provide: Clipboard,
+            deps: [CoreAppProvider],
+            useFactory: (appProvider: CoreAppProvider) => {
+                return appProvider.isMobile() ? new Clipboard() : new ClipboardMock(appProvider);
+            }
+        },
+        {
+            provide: Globalization,
+            deps: [CoreAppProvider],
+            useFactory: (appProvider: CoreAppProvider) => {
+                return appProvider.isMobile() ? new Globalization() : new GlobalizationMock(appProvider);
+            }
+        },
+        {
+            provide: Network,
+            deps: [Platform],
+            useFactory: (platform: Platform) => {
+                // Use platform instead of CoreAppProvider to prevent circular dependencies.
+                return platform.is('cordova') ? new Network() : new NetworkMock();
+            }
+        }
+    ]
+})
+export class CoreEmulatorModule {
+    constructor(appProvider: CoreAppProvider) {
+        let win = window; // Convert the "window" to "any" type to be able to use non-standard properties.
+
+        // Emulate Custom URL Scheme plugin in desktop apps.
+        if (appProvider.isDesktop()) {
+            require('electron').ipcRenderer.on('mmAppLaunched', function(event, url) {
+                win.handleOpenURL && win.handleOpenURL(url);
+            });
+        }
+    }
+}
diff --git a/src/core/emulator/providers/clipboard.ts b/src/core/emulator/providers/clipboard.ts
new file mode 100644
index 000000000..89a427f8e
--- /dev/null
+++ b/src/core/emulator/providers/clipboard.ts
@@ -0,0 +1,102 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { Clipboard } from '@ionic-native/clipboard';
+import { CoreAppProvider } from '../../../providers/app';
+
+/**
+ * Emulates the Cordova Clipboard plugin in desktop apps and in browser.
+ */
+@Injectable()
+export class ClipboardMock extends Clipboard {
+    isDesktop: boolean;
+    clipboard: any;
+    copyTextarea: HTMLTextAreaElement;
+
+    constructor(appProvider: CoreAppProvider) {
+        super();
+
+        this.isDesktop = appProvider.isDesktop();
+        if (this.isDesktop) {
+            this.clipboard = require('electron').clipboard;
+        } else {
+            // In browser the text must be selected in order to copy it. Create a hidden textarea to put the text in it.
+            this.copyTextarea = document.createElement('textarea');
+            this.copyTextarea.className = 'mm-browser-copy-area';
+            this.copyTextarea.setAttribute('aria-hidden', 'true');
+            document.body.appendChild(this.copyTextarea);
+        }
+    }
+
+    /**
+     * Copy some text to the clipboard.
+     *
+     * @param {string} text The text to copy.
+     * @return {Promise} Promise resolved when copied.
+     */
+    copy(text: string) : Promise {
+        return new Promise((resolve, reject) => {
+            if (this.isDesktop) {
+                this.clipboard.writeText(text);
+                resolve();
+            } else {
+                // Put the text in the hidden textarea and select it.
+                this.copyTextarea.innerHTML = text;
+                this.copyTextarea.select();
+
+                try {
+                    if (document.execCommand('copy')) {
+                        resolve();
+                    } else {
+                        reject();
+                    }
+                } catch (err) {
+                    reject();
+                }
+
+                this.copyTextarea.innerHTML = '';
+            }
+        });
+    }
+
+    /*
+     * Get the text stored in the clipboard.
+     *
+     * @return {Promise} Promise resolved with the text.
+     */
+    paste() : Promise {
+        return new Promise((resolve, reject) => {
+            if (this.isDesktop) {
+                resolve(this.clipboard.readText());
+            } else {
+                // Paste the text in the hidden textarea and get it.
+                this.copyTextarea.innerHTML = '';
+                this.copyTextarea.select();
+
+                try {
+                    if (document.execCommand('paste')) {
+                        resolve(this.copyTextarea.innerHTML);
+                    } else {
+                        reject();
+                    }
+                } catch (err) {
+                    reject();
+                }
+
+                this.copyTextarea.innerHTML = '';
+            }
+        });
+    }
+}
diff --git a/src/core/emulator/providers/globalization.ts b/src/core/emulator/providers/globalization.ts
new file mode 100644
index 000000000..5ffa2e142
--- /dev/null
+++ b/src/core/emulator/providers/globalization.ts
@@ -0,0 +1,74 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { Globalization } from '@ionic-native/globalization';
+import { CoreAppProvider } from '../../../providers/app';
+
+/**
+ * Emulates the Cordova Globalization plugin in desktop apps and in browser.
+ */
+@Injectable()
+export class GlobalizationMock extends Globalization {
+
+    constructor(private appProvider: CoreAppProvider) {
+        super();
+    }
+
+    /**
+     * Get the current locale.
+     *
+     * @return {string} Locale name.
+     */
+    private getCurrentlocale() : string {
+        // Get browser language.
+        var navLang = (navigator).userLanguage || navigator.language;
+
+        try {
+            if (this.appProvider.isDesktop()) {
+                var locale = require('electron').remote.app.getLocale();
+                return locale || navLang;
+            } else {
+                return navLang;
+            }
+        } catch(ex) {
+            // Something went wrong, return browser language.
+            return navLang;
+        }
+    }
+
+    /**
+     * Get the current locale name.
+     *
+     * @return {Promise<{value: string}>} Promise resolved with an object with the language string.
+     */
+    getLocaleName() : Promise<{value: string}> {
+        var locale = this.getCurrentlocale();
+        if (locale) {
+            return Promise.resolve({value: locale});
+        } else {
+            var error = {code: GlobalizationError.UNKNOWN_ERROR, message: 'Cannot get language'};
+            return Promise.reject(error);
+        }
+    }
+
+    /*
+     * Get the current preferred language.
+     *
+     * @return {Promise<{value: string}>} Promise resolved with an object with the language string.
+     */
+    getPreferredLanguage() : Promise<{value: string}> {
+        return this.getLocaleName();
+    }
+}
diff --git a/src/core/emulator/providers/network.ts b/src/core/emulator/providers/network.ts
new file mode 100644
index 000000000..c6dfa36da
--- /dev/null
+++ b/src/core/emulator/providers/network.ts
@@ -0,0 +1,79 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { Network } from '@ionic-native/network';
+import { Observable, Subject } from 'rxjs';
+
+/**
+ * Emulates the Cordova Globalization plugin in desktop apps and in browser.
+ */
+@Injectable()
+export class NetworkMock extends Network {
+    type: null;
+
+    constructor() {
+        super();
+
+        (window).Connection = {
+            UNKNOWN: 'unknown',
+            ETHERNET: 'ethernet',
+            WIFI: 'wifi',
+            CELL_2G: '2g',
+            CELL_3G: '3g',
+            CELL_4G: '4g',
+            CELL: 'cellular',
+            NONE: 'none'
+        };
+    }
+
+    /**
+    * Returns an observable to watch connection changes.
+    *
+    * @return {Observable} Observable.
+    */
+    onchange(): Observable {
+        return Observable.merge(this.onConnect(), this.onDisconnect());
+    }
+
+    /**
+    * Returns an observable to notify when the app is connected.
+    *
+    * @return {Observable} Observable.
+    */
+    onConnect() : Observable {
+        let observable = new Subject();
+
+        window.addEventListener('online', (ev) => {
+            observable.next(ev);
+        }, false);
+
+        return observable;
+    }
+
+    /**
+    * Returns an observable to notify when the app is disconnected.
+    *
+    * @return {Observable} Observable.
+    */
+    onDisconnect() : Observable {
+        let observable = new Subject();
+
+        window.addEventListener('offline', (ev) => {
+            observable.next(ev);
+        }, false);
+
+        return observable;
+    }
+}
diff --git a/src/providers/db.ts b/src/providers/db.ts
index 4e807a9de..0a463143e 100644
--- a/src/providers/db.ts
+++ b/src/providers/db.ts
@@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
 import { SQLite } from '@ionic-native/sqlite';
 import { Platform } from 'ionic-angular';
 import { SQLiteDB } from '../classes/sqlitedb';
+import { SQLiteDBMock } from '../core/emulator/classes/sqlitedb';
 
 /**
  * This service allows interacting with the local database to store and retrieve data.
@@ -38,7 +39,11 @@ export class CoreDbProvider {
      */
     getDB(name: string, forceNew?: boolean) : SQLiteDB {
         if (typeof this.dbInstances[name] === 'undefined' || forceNew) {
-            this.dbInstances[name] = new SQLiteDB(name, this.sqlite, this.platform);
+            if (this.platform.is('cordova')) {
+                this.dbInstances[name] = new SQLiteDB(name, this.sqlite, this.platform);
+            } else {
+                this.dbInstances[name] = new SQLiteDBMock(name);
+            }
         }
         return this.dbInstances[name];
     }
@@ -60,15 +65,18 @@ export class CoreDbProvider {
         }
 
         return promise.then(() => {
+            let db = this.dbInstances[name];
             delete this.dbInstances[name];
 
-            return this.sqlite.deleteDatabase({
-                name: name,
-                location: 'default'
-            });
+            if (this.platform.is('cordova')) {
+                return this.sqlite.deleteDatabase({
+                    name: name,
+                    location: 'default'
+                });
+            } else {
+                // In WebSQL we cannot delete the database, just empty it.
+                return db.emptyDatabase();
+            }
         });
     }
 }
-
-
-