1 // This file is part of Moodle - http://moodle.org/
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.
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/>.
17 * Fetch and return language strings.
20 * @copyright 2015 Damyon Wiese <damyon@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 import $ from 'jquery';
26 import Ajax from 'core/ajax';
27 import Config from 'core/config';
28 import LocalStorage from 'core/localstorage';
31 * @typedef StringRequest
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.
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 */
46 * Return a Promise that resolves to a string.
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.
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
57 * @example <caption>Fetching a string</caption>
59 * import {getString} from 'core/str';
60 * get_string('cannotfindteacher', 'error')
61 * .then((str) => window.console.log(str)); // Cannot find teacher
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]);
70 * Return a Promise that resolves to a string.
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.
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
81 * @example <caption>Fetching a string</caption>
83 * import {getString} from 'core/str';
85 * getString('cannotfindteacher', 'error')
86 * .then((str) => window.console.log(str)); // Cannot find teacher
88 export const getString = (key, component, param, lang) =>
89 getRequestedStrings([{key, component, param, lang}])[0];
92 * Make a batch request to load a set of strings.
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.
97 * @param {Array.<StringRequest>} requests List of strings to fetch
98 * @return {Promise<string[]>} A native promise containing an array of the translated strings
100 * @example <caption>Fetching a set of strings</caption>
102 * import {getStrings} from 'core/str';
105 * key: 'cannotfindteacher',
106 * component: 'error',
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
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[]}
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;
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;
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));
160 // Check if the string is in the browser's local storage.
161 const cached = LocalStorage.get(cacheKey);
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));
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);
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) => {
179 methodname: 'core_get_string',
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));
200 if (requestData.length) {
201 // If we need to load any strings from the server then send
203 Ajax.call(requestData, true, false, false, 0, M.cfg.langrev);
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';
223 * key: 'cannotfindteacher',
224 * component: 'error',
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
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
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.
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] = {};
273 M.str[component][key] = value;
276 // Check local storage.
277 if (!LocalStorage.get(cacheKey)) {
278 LocalStorage.set(cacheKey, value);
281 // Check the promises cache.
282 if (!(cacheKey in promiseCache)) {
283 promiseCache[cacheKey] = $.Deferred().resolve(value).promise();
287 /* eslint-enable no-restricted-properties */