From 7e8332d978d2da3f01473a139e187bd1ec7f91af Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Thu, 17 May 2018 12:46:24 +0200
Subject: [PATCH 1/2] MOBILE-2413 lang: Fix custom and plugins strings

---
 src/app/app.component.ts |  29 ++++++++-
 src/providers/lang.ts    | 126 +++++++++++++++++++++++++++++++--------
 2 files changed, 128 insertions(+), 27 deletions(-)

diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index e3ba3417f..22f8c0aec 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -18,7 +18,9 @@ import { StatusBar } from '@ionic-native/status-bar';
 import { SplashScreen } from '@ionic-native/splash-screen';
 import { CoreAppProvider } from '@providers/app';
 import { CoreEventsProvider } from '@providers/events';
+import { CoreLangProvider } from '@providers/lang';
 import { CoreLoggerProvider } from '@providers/logger';
+import { CoreSitesProvider } from '@providers/sites';
 import { CoreLoginHelperProvider } from '@core/login/providers/helper';
 
 @Component({
@@ -33,7 +35,7 @@ export class MoodleMobileApp implements OnInit {
 
     constructor(private platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, logger: CoreLoggerProvider,
         private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider,
-        private appProvider: CoreAppProvider) {
+        private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider) {
         this.logger = logger.getInstance('AppComponent');
 
         platform.ready().then(() => {
@@ -49,9 +51,12 @@ export class MoodleMobileApp implements OnInit {
      * Component being initialized.
      */
     ngOnInit(): void {
-        // Go to sites page when user is logged out.
         this.eventsProvider.on(CoreEventsProvider.LOGOUT, () => {
+            // Go to sites page when user is logged out.
             this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage');
+
+            // Unload lang custom strings.
+            this.langProvider.clearCustomStrings();
         });
 
         // Listen for session expired events.
@@ -111,5 +116,25 @@ export class MoodleMobileApp implements OnInit {
         this.eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, (url) => {
             this.loginHelper.appLaunchedByURL(url);
         });
+
+        // Load custom lang strings. This cannot be done inside the lang provider because it causes circular dependencies.
+        const loadCustomStrings = (): void => {
+            const currentSite = this.sitesProvider.getCurrentSite(),
+                customStrings = currentSite && currentSite.getStoredConfig('tool_mobile_customlangstrings');
+
+            if (typeof customStrings != 'undefined') {
+                this.langProvider.loadCustomStrings(customStrings);
+            }
+        };
+
+        this.eventsProvider.on(CoreEventsProvider.LOGIN, () => {
+            loadCustomStrings();
+        });
+
+        this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, (siteId) => {
+            if (siteId == this.sitesProvider.getCurrentSiteId()) {
+                loadCustomStrings();
+            }
+        });
     }
 }
diff --git a/src/providers/lang.ts b/src/providers/lang.ts
index 6207168de..038001174 100644
--- a/src/providers/lang.ts
+++ b/src/providers/lang.ts
@@ -53,13 +53,10 @@ export class CoreLangProvider {
      * @param {string} [prefix] A prefix to add to all keys.
      */
     addSitePluginsStrings(lang: string, strings: any, prefix?: string): void {
-        // Initialize structures if they don't exist.
+        // Initialize structure if it doesn't exist.
         if (!this.sitePluginsStrings[lang]) {
             this.sitePluginsStrings[lang] = {};
         }
-        if (!this.translate.translations[lang]) {
-            this.translate.translations[lang] = {};
-        }
 
         for (const key in strings) {
             const prefixedKey = prefix + key;
@@ -75,19 +72,8 @@ export class CoreLangProvider {
             // Make sure we didn't add to many brackets in some case.
             value = value.replace(/{{{([^ ]+)}}}/gm, '{{$1}}');
 
-            if (!this.sitePluginsStrings[lang][prefixedKey]) {
-                // It's a new site plugin string. Store the original value.
-                this.sitePluginsStrings[lang][prefixedKey] = {
-                    original: this.translate.translations[lang][prefixedKey],
-                    value: value
-                };
-            } else {
-                // Site plugin string already defined. Store the new value.
-                this.sitePluginsStrings[lang][prefixedKey].value = value;
-            }
-
-            // Store the string in the translations table.
-            this.translate.translations[lang][prefixedKey] = value;
+            // Load the string.
+            this.loadString(this.sitePluginsStrings, lang, prefixedKey, value);
         }
     }
 
@@ -100,13 +86,38 @@ export class CoreLangProvider {
     changeCurrentLanguage(language: string): Promise<any> {
         const promises = [];
 
-        promises.push(this.translate.use(language));
+        // Change the language, resolving the promise when we receive the first value.
+        promises.push(new Promise((resolve, reject): void => {
+            const subscription = this.translate.use(language).subscribe((data) => {
+                resolve(data);
+
+                // Data received, unsubscribe. Use a timeout because we can receive a value immediately.
+                setTimeout(() => {
+                    subscription.unsubscribe();
+                });
+            }, (error) => {
+                reject(error);
+
+                // Error received, unsubscribe. Use a timeout because we can receive a value immediately.
+                setTimeout(() => {
+                    subscription.unsubscribe();
+                });
+            });
+        }));
+
+        // Change the config.
         promises.push(this.configProvider.set('current_language', language));
 
         moment.locale(language);
         this.currentLanguage = language;
 
-        return Promise.all(promises);
+        return Promise.all(promises).finally(() => {
+            // Load the custom and site plugins strings for the language.
+            if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) {
+                // Some lang strings have changed, emit an event to update the pipes.
+                this.translate.onLangChange.emit({lang: language, translations: this.translate.translations[language]});
+            }
+        });
     }
 
     /**
@@ -227,15 +238,75 @@ export class CoreLangProvider {
                 this.customStrings[lang] = {};
             }
 
-            // Store the original value of the custom string.
-            this.customStrings[lang][values[0]] = {
-                original: this.translate.translations[lang][values[0]],
-                value: values[1]
+            // Convert old keys format to new one.
+            const key = values[0].replace(/^mm\.core/, 'core').replace(/^mm\./, 'core.').replace(/^mma\./, 'addon.')
+                    .replace(/^core\.sidemenu/, 'core.mainmenu').replace(/^addon\.grades/, 'core.grades')
+                    .replace(/^addon\.participants/, 'core.user');
+
+            this.loadString(this.customStrings, lang, key, values[1]);
+        });
+
+        this.customStringsRaw = strings;
+    }
+
+    /**
+     * Load custom strings for a certain language that weren't loaded because the language wasn't active.
+     *
+     * @param {any} langObject The object with the strings to load.
+     * @param {string} lang Language to load.
+     * @return {boolean} Whether the translation table was modified.
+     */
+    loadLangStrings(langObject: any, lang: string): boolean {
+        let langApplied = false;
+
+        if (langObject[lang]) {
+            for (const key in langObject[lang]) {
+                const entry = langObject[lang][key];
+
+                if (!entry.applied) {
+                    // Store the original value of the string.
+                    entry.original = this.translate.translations[lang][key];
+
+                    // Store the string in the translations table.
+                    this.translate.translations[lang][key] = entry.value;
+
+                    entry.applied = true;
+                    langApplied = true;
+                }
+            }
+        }
+
+        return langApplied;
+    }
+
+    /**
+     * Load a string in a certain lang object and in the translate table if the lang is loaded.
+     *
+     * @param {any} langObject The object where to store the lang.
+     * @param {string} lang Language code.
+     * @param {string} key String key.
+     * @param {string} value String value.
+     */
+    loadString(langObject: any, lang: string, key: string, value: string): void {
+        if (this.translate.translations[lang]) {
+            // The language is loaded.
+            // Store the original value of the string.
+            langObject[lang][key] = {
+                original: this.translate.translations[lang][key],
+                value: value,
+                applied: true
             };
 
             // Store the string in the translations table.
-            this.translate.translations[lang][values[0]] = values[1];
-        });
+            this.translate.translations[lang][key] = value;
+        } else {
+            // The language isn't loaded.
+            // Save it in our object but not in the translations table, it will be loaded when the lang is loaded.
+            langObject[lang][key] = {
+                value: value,
+                applied: false
+            };
+        }
     }
 
     /**
@@ -246,6 +317,11 @@ export class CoreLangProvider {
     protected unloadStrings(strings: any): void {
         // Iterate over all languages and strings.
         for (const lang in strings) {
+            if (!this.translate.translations[lang]) {
+                // Language isn't loaded, nothing to unload.
+                continue;
+            }
+
             const langStrings = strings[lang];
             for (const key in langStrings) {
                 const entry = langStrings[key];

From 6ac7988948c63b728335a876a9497081a70446b5 Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Thu, 17 May 2018 12:47:07 +0200
Subject: [PATCH 2/2] MOBILE-2413 ui: Use popover interface in all selects

---
 src/addon/mod/feedback/components/index/index.html              | 2 +-
 src/addon/mod/feedback/pages/form/form.html                     | 2 +-
 src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html | 2 +-
 src/addon/mod/feedback/pages/respondents/respondents.html       | 2 +-
 src/addon/userprofilefield/menu/component/menu.html             | 2 +-
 src/core/settings/pages/general/general.html                    | 2 +-
 6 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/addon/mod/feedback/components/index/index.html b/src/addon/mod/feedback/components/index/index.html
index ccefec125..4f202fd92 100644
--- a/src/addon/mod/feedback/components/index/index.html
+++ b/src/addon/mod/feedback/components/index/index.html
@@ -47,7 +47,7 @@
         <ion-item text-wrap *ngIf="access.canedititems && (groupInfo.separateGroups || groupInfo.visibleGroups)">
             <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
             <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
-            <ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-feedback-groupslabel">
+            <ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-feedback-groupslabel" interface="popover">
                 <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
             </ion-select>
         </ion-item>
diff --git a/src/addon/mod/feedback/pages/form/form.html b/src/addon/mod/feedback/pages/form/form.html
index 722813335..c67fc5376 100644
--- a/src/addon/mod/feedback/pages/form/form.html
+++ b/src/addon/mod/feedback/pages/form/form.html
@@ -49,7 +49,7 @@
                                     </ion-item>
                                 </ion-list>
                                 <ng-container *ngSwitchCase="'multichoice-d'">
-                                    <ion-select [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value">
+                                    <ion-select [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value" interface="popover">
                                         <ion-option *ngFor="let option of item.choices" [value]="option.value"><core-format-text  [component]="component" [componentId]="componentId" [text]="option.label"></core-format-text></ion-option>
                                     </ion-select>
                                 </ng-container>
diff --git a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html
index 906dfbd6b..180f3339a 100644
--- a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html
+++ b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html
@@ -12,7 +12,7 @@
             <ion-item text-wrap *ngIf="groupInfo.separateGroups || groupInfo.visibleGroups">
                 <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
                 <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
-                <ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)" aria-labelledby="addon-feedback-groupslabel">
+                <ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)" aria-labelledby="addon-feedback-groupslabel" interface="popover">
                     <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
                 </ion-select>
             </ion-item>
diff --git a/src/addon/mod/feedback/pages/respondents/respondents.html b/src/addon/mod/feedback/pages/respondents/respondents.html
index f9e2c2dd5..1754bde05 100644
--- a/src/addon/mod/feedback/pages/respondents/respondents.html
+++ b/src/addon/mod/feedback/pages/respondents/respondents.html
@@ -13,7 +13,7 @@
                 <ion-item text-wrap *ngIf="groupInfo.separateGroups || groupInfo.visibleGroups">
                     <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
                     <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
-                    <ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)" aria-labelledby="addon-feedback-groupslabel">
+                    <ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)" aria-labelledby="addon-feedback-groupslabel" interface="popover">
                         <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
                     </ion-select>
                 </ion-item>
diff --git a/src/addon/userprofilefield/menu/component/menu.html b/src/addon/userprofilefield/menu/component/menu.html
index 68198ddc9..39f54dbc8 100644
--- a/src/addon/userprofilefield/menu/component/menu.html
+++ b/src/addon/userprofilefield/menu/component/menu.html
@@ -6,7 +6,7 @@
 <!-- Edit. -->
 <ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form">
     <ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label>
-    <ion-select [formControlName]="field.modelName" [placeholder]="'core.choosedots' | translate" core-input-errors>
+    <ion-select [formControlName]="field.modelName" [placeholder]="'core.choosedots' | translate" core-input-errors interface="popover">
         <ion-option value="">{{ 'core.choosedots' | translate }}</ion-option>
         <ion-option *ngFor="let option of field.options" [value]="option">{{option}}</ion-option>
     </ion-select>
diff --git a/src/core/settings/pages/general/general.html b/src/core/settings/pages/general/general.html
index ea82aa063..d1be96153 100644
--- a/src/core/settings/pages/general/general.html
+++ b/src/core/settings/pages/general/general.html
@@ -6,7 +6,7 @@
 <ion-content>
     <ion-item text-wrap>
         <ion-label><h2>{{ 'core.settings.language' | translate }}</h2></ion-label>
-        <ion-select [(ngModel)]="selectedLanguage" (ngModelChange)="languageChanged()">
+        <ion-select [(ngModel)]="selectedLanguage" (ngModelChange)="languageChanged()" interface="popover">
             <ion-option *ngFor="let code of languageCodes" [value]="code">{{ languages[code] }}</ion-option>
         </ion-select>
     </ion-item>