Merge branch 'MDL-81713-main' of https://github.com/junpataleta/moodle
[moodle.git] / lib / amd / src / str.js
bloba09dcd00be642caac5dc4a80c1fc71b6ef640e33
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.
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 return language strings.
18  *
19  * @module     core/str
20  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  * @since      2.9
23  *
24  */
25 import $ from 'jquery';
26 import Ajax from 'core/ajax';
27 import Config from 'core/config';
28 import LocalStorage from 'core/localstorage';
30 /**
31  * @typedef StringRequest
32  * @type {object}
33  * @param {string} requests.key The string identifer to fetch
34  * @param {string} [requests.component='core'] The componet to fetch from
35  * @param {string} [requests.lang] The language to fetch a string for. Defaults to current page language.
36  * @param {object|string} [requests.param] The param for variable expansion in the string.
37  */
39 // Module cache for the promises so that we don't make multiple
40 // unnecessary requests.
41 let promiseCache = [];
43 /* eslint-disable no-restricted-properties */
45 /**
46  * Return a Promise that resolves to a string.
47  *
48  * If the string has previously been cached, then the Promise will be resolved immediately, otherwise it will be fetched
49  * from the server and resolved when available.
50  *
51  * @param {string} key The language string key
52  * @param {string} [component='core'] The language string component
53  * @param {object|string} [param] The param for variable expansion in the string.
54  * @param {string} [lang] The users language - if not passed it is deduced.
55  * @return {jQuery.Promise} A jQuery Promise containing the translated string
56  *
57  * @example <caption>Fetching a string</caption>
58  *
59  * import {getString} from 'core/str';
60  * get_string('cannotfindteacher', 'error')
61  * .then((str) => window.console.log(str)); // Cannot find teacher
62  */
63 // eslint-disable-next-line camelcase
64 export const get_string = (key, component, param, lang) => {
65     return get_strings([{key, component, param, lang}])
66         .then(results => results[0]);
69 /**
70  * Return a Promise that resolves to a string.
71  *
72  * If the string has previously been cached, then the Promise will be resolved immediately, otherwise it will be fetched
73  * from the server and resolved when available.
74  *
75  * @param {string} key The language string key
76  * @param {string} [component='core'] The language string component
77  * @param {object|string} [param] The param for variable expansion in the string.
78  * @param {string} [lang] The users language - if not passed it is deduced.
79  * @return {Promise<string>} A native Promise containing the translated string
80  *
81  * @example <caption>Fetching a string</caption>
82  *
83  * import {getString} from 'core/str';
84  *
85  * getString('cannotfindteacher', 'error')
86  * .then((str) => window.console.log(str)); // Cannot find teacher
87  */
88 export const getString = (key, component, param, lang) =>
89     getRequestedStrings([{key, component, param, lang}])[0];
91 /**
92  * Make a batch request to load a set of strings.
93  *
94  * Any missing string will be fetched from the server.
95  * The Promise will only be resolved once all strings are available, or an attempt has been made to fetch them.
96  *
97  * @param {Array.<StringRequest>} requests List of strings to fetch
98  * @return {Promise<string[]>} A native promise containing an array of the translated strings
99  *
100  * @example <caption>Fetching a set of strings</caption>
102  * import {getStrings} from 'core/str';
103  * getStrings([
104  *     {
105  *         key: 'cannotfindteacher',
106  *         component: 'error',
107  *     },
108  *     {
109  *         key: 'yes',
110  *         component: 'core',
111  *     },
112  *     {
113  *         key: 'no',
114  *         component: 'core',
115  *     },
116  * ])
117  * .then((cannotFindTeacher, yes, no) => {
118  *     window.console.log(cannotFindTeacher); // Cannot find teacher
119  *     window.console.log(yes); // Yes
120  *     window.console.log(no); // No
121  * });
122  */
123 export const getStrings = (requests) => Promise.all(getRequestedStrings(requests));
126  * Internal function to perform the string requests.
128  * @param {Array.<StringRequest>} requests List of strings to fetch
129  * @returns {Promise[]}
130  */
131 const getRequestedStrings = (requests) => {
132     let requestData = [];
133     const pageLang = Config.language;
135     // Helper function to construct the cache key.
136     const getCacheKey = ({key, component, lang = pageLang}) => `core_str/${key}/${component}/${lang}`;
138     const stringPromises = requests.map((request) => {
139         let {component, key, param, lang = pageLang} = request;
140         if (!component) {
141             component = 'core';
142         }
144         const cacheKey = getCacheKey({key, component, lang});
146         // Helper function to add the promise to cache.
147         const buildReturn = (promise) => {
148             // Make sure the promise cache contains our promise.
149             promiseCache[cacheKey] = promise;
150             return promise;
151         };
153         // Check if we can serve the string straight from M.str.
154         if (component in M.str && key in M.str[component]) {
155             return buildReturn(new Promise((resolve) => {
156                 resolve(M.util.get_string(key, component, param, lang));
157             }));
158         }
160         // Check if the string is in the browser's local storage.
161         const cached = LocalStorage.get(cacheKey);
162         if (cached) {
163             M.str[component] = {...M.str[component], [key]: cached};
164             return buildReturn(new Promise((resolve) => {
165                 resolve(M.util.get_string(key, component, param, lang));
166             }));
167         }
169         // Check if we've already loaded this string from the server.
170         if (cacheKey in promiseCache) {
171             return buildReturn(promiseCache[cacheKey]).then(() => {
172                 return M.util.get_string(key, component, param, lang);
173             });
174         } else {
175             // We're going to have to ask the server for the string so
176             // add this string to the list of requests to be sent.
177             return buildReturn(new Promise((resolve, reject) => {
178                 requestData.push({
179                     methodname: 'core_get_string',
180                     args: {
181                         stringid: key,
182                         stringparams: [],
183                         component,
184                         lang,
185                     },
186                     done: (str) => {
187                         // When we get the response from the server
188                         // we should update M.str and the browser's
189                         // local storage before resolving this promise.
190                         M.str[component] = {...M.str[component], [key]: str};
191                         LocalStorage.set(cacheKey, str);
192                         resolve(M.util.get_string(key, component, param, lang));
193                     },
194                     fail: reject
195                 });
196             }));
197         }
198     });
200     if (requestData.length) {
201         // If we need to load any strings from the server then send
202         // off the request.
203         Ajax.call(requestData, true, false, false, 0, M.cfg.langrev);
204     }
206     return stringPromises;
210  * Make a batch request to load a set of strings.
212  * Any missing string will be fetched from the server.
213  * The Promise will only be resolved once all strings are available, or an attempt has been made to fetch them.
215  * @param {Array.<StringRequest>} requests List of strings to fetch
216  * @return {jquery.Promise<string[]>} A jquery promise containing an array of the translated strings
218  * @example <caption>Fetching a set of strings</caption>
220  * import {getStrings} from 'core/str';
221  * get_strings([
222  *     {
223  *         key: 'cannotfindteacher',
224  *         component: 'error',
225  *     },
226  *     {
227  *         key: 'yes',
228  *         component: 'core',
229  *     },
230  *     {
231  *         key: 'no',
232  *         component: 'core',
233  *     },
234  * ])
235  * .then((cannotFindTeacher, yes, no) => {
236  *     window.console.log(cannotFindTeacher); // Cannot find teacher
237  *     window.console.log(yes); // Yes
238  *     window.console.log(no); // No
239  * });
240  */
241 // eslint-disable-next-line camelcase
242 export const get_strings = (requests) => {
243     // We need to use jQuery here because some calling code uses the
244     // .done handler instead of the .then handler.
245     return $.when.apply($, getRequestedStrings(requests))
246         .then((...strings) => strings);
250  * Add a list of strings to the caches.
252  * This function should typically only be called from core APIs to pre-cache values.
254  * @method cache_strings
255  * @protected
256  * @param {Object[]} strings List of strings to fetch
257  * @param {string} strings.key The string identifer to fetch
258  * @param {string} strings.value The string value
259  * @param {string} [strings.component='core'] The componet to fetch from
260  * @param {string} [strings.lang=Config.language] The language to fetch a string for. Defaults to current page language.
261  */
262 // eslint-disable-next-line camelcase
263 export const cache_strings = (strings) => {
264     strings.forEach(({key, component, value, lang = Config.language}) => {
265         const cacheKey = ['core_str', key, component, lang].join('/');
267         // Check M.str caching.
268         if (!(component in M.str) || !(key in M.str[component])) {
269             if (!(component in M.str)) {
270                 M.str[component] = {};
271             }
273             M.str[component][key] = value;
274         }
276         // Check local storage.
277         if (!LocalStorage.get(cacheKey)) {
278             LocalStorage.set(cacheKey, value);
279         }
281         // Check the promises cache.
282         if (!(cacheKey in promiseCache)) {
283             promiseCache[cacheKey] = $.Deferred().resolve(value).promise();
284         }
285     });
287 /* eslint-enable no-restricted-properties */