Merge branch 'MDL-57636_master' of https://github.com/dasistwas/moodle
[moodle.git] / lib / amd / src / str.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Fetch and render language strings.
18  * Hooks into the old M.str global - but can also fetch missing strings on the fly.
19  *
20  * @module     core/str
21  * @class      str
22  * @package    core
23  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  * @since      2.9
26  */
27 // Disable no-restriced-properties because M.str is expected here:
28 /* eslint-disable no-restricted-properties */
29 define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage) {
31     var promiseCache = [];
33     return /** @alias module:core/str */ {
34         // Public variables and functions.
35         /**
36          * Return a promise object that will be resolved into a string eventually (maybe immediately).
37          *
38          * @method get_string
39          * @param {string} key The language string key
40          * @param {string} component The language string component
41          * @param {string} param The param for variable expansion in the string.
42          * @param {string} lang The users language - if not passed it is deduced.
43          * @return {Promise}
44          */
45          // eslint-disable-next-line camelcase
46         get_string: function(key, component, param, lang) {
47             var request = this.get_strings([{
48                 key: key,
49                 component: component,
50                 param: param,
51                 lang: lang
52             }]);
54             return request.then(function(results) {
55                 return results[0];
56             });
57         },
59         /**
60          * Make a batch request to load a set of strings
61          *
62          * @method get_strings
63          * @param {Object[]} requests Array of { key: key, component: component, param: param, lang: lang };
64          *                                      See get_string for more info on these args.
65          * @return {Promise}
66          */
67          // eslint-disable-next-line camelcase
68         get_strings: function(requests) {
70             var deferred = $.Deferred();
71             var results = [];
72             var i = 0;
73             var missing = false;
74             var request;
76             // Try from local storage. If it's there - put it in M.str and resolve it.
78             for (i = 0; i < requests.length; i++) {
79                 request = requests[i];
80                 if (typeof request.lang === "undefined") {
81                     request.lang = $('html').attr('lang').replace(/-/g, '_');
82                 }
83                 request.cacheKey = 'core_str/' + request.key + '/' + request.component + '/' + request.lang;
84                 if (typeof M.str[request.component] === "undefined" ||
85                         typeof M.str[request.component][request.key] === "undefined") {
86                     // Try and revive it from local storage.
87                     var cached = storage.get(request.cacheKey);
88                     if (cached) {
89                         if (typeof M.str[request.component] === "undefined") {
90                             M.str[request.component] = [];
91                         }
92                         M.str[request.component][request.key] = cached;
93                     } else {
94                         // It's really not here.
95                         missing = true;
96                     }
97                 }
98             }
100             if (!missing) {
101                 // We have all the strings already.
102                 for (i = 0; i < requests.length; i++) {
103                     request = requests[i];
105                     results[i] = M.util.get_string(request.key, request.component, request.param);
106                 }
107                 deferred.resolve(results);
108             } else {
109                 var ajaxrequests = [];
110                 var fetchpromises = [];
112                 // Done handler for ajax call. Must be bound to the current fetchpromise. We do this
113                 // to avoid creating a function in a loop.
114                 var doneFunc = function(str) {
115                     this.resolve(str);
116                 };
118                 var failFunc = function(reason) {
119                     this.reject(reason);
120                 };
122                 for (i = 0; i < requests.length; i++) {
123                     request = requests[i];
125                     // If we ever fetched this string with a promise, reuse it.
126                     if (typeof promiseCache[request.cacheKey] !== 'undefined') {
127                         fetchpromises.push(promiseCache[request.cacheKey]);
128                     } else {
129                         // Add this to the list we need to really fetch.
130                         var fetchpromise = $.Deferred();
132                         ajaxrequests.push({
133                             methodname: 'core_get_string',
134                             args: {
135                                 stringid: request.key,
136                                 component: request.component,
137                                 lang: request.lang,
138                                 stringparams: []
139                             },
140                             done: doneFunc.bind(fetchpromise),
141                             fail: failFunc.bind(fetchpromise)
142                         });
144                         promiseCache[request.cacheKey] = fetchpromise.promise();
145                         fetchpromises.push(promiseCache[request.cacheKey]);
146                     }
147                 }
149                 // Everything might already be queued so we need to check if we have real ajax requests to run.
150                 if (ajaxrequests.length > 0) {
151                     ajax.call(ajaxrequests, true, false);
152                 }
154                 $.when.apply(null, fetchpromises).done(
155                     function() {
156                         // Turn the list of arguments (unknown length) into a real array.
157                         var i = 0;
158                         for (i = 0; i < arguments.length; i++) {
159                             request = requests[i];
160                             // Cache all the string templates.
161                             if (typeof M.str[request.component] === "undefined") {
162                                 M.str[request.component] = [];
163                             }
164                             M.str[request.component][request.key] = arguments[i];
165                             storage.set('core_str/' + request.key + '/' + request.component + '/' + request.lang, arguments[i]);
166                             // And set the results.
167                             results[i] = M.util.get_string(request.key, request.component, request.param).trim();
168                         }
169                         deferred.resolve(results);
170                     }
171                 ).fail(
172                     function(ex) {
173                         deferred.reject(ex);
174                     }
175                 );
176             }
178             return deferred.promise();
179         },
180         /**
181          * Add a list of strings to the caches.
182          *
183          * @method cache_strings
184          * @param {Object[]} strings Array of { key: key, component: component, lang: lang, value: value }
185          */
186          // eslint-disable-next-line camelcase
187         cache_strings: function(strings) {
188             var defaultLang = $('html').attr('lang').replace(/-/g, '_');
189             strings.forEach(function(string) {
190                 var lang = !(lang in string) ? defaultLang : string.lang;
191                 var key = string.key;
192                 var component = string.component;
193                 var value = string.value;
194                 var cacheKey = ['core_str', key, component, lang].join('/');
196                 // Check M.str caching.
197                 if (!(component in M.str) || !(key in M.str[component])) {
198                     if (!(component in M.str)) {
199                         M.str[component] = {};
200                     }
202                     M.str[component][key] = value;
203                 }
205                 // Check local storage.
206                 if (!storage.get(cacheKey)) {
207                     storage.set(cacheKey, value);
208                 }
210                 // Check the promises cache.
211                 if (!(cacheKey in promiseCache)) {
212                     promiseCache[cacheKey] = $.Deferred().resolve(value).promise();
213                 }
214             });
215         }
216     };
217 });