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