diff --git a/lib/editor/tiny/amd/build/editor.min.js b/lib/editor/tiny/amd/build/editor.min.js index 09de3bd28bba..60ebfb3a3e14 100644 --- a/lib/editor/tiny/amd/build/editor.min.js +++ b/lib/editor/tiny/amd/build/editor.min.js @@ -1,3 +1,3 @@ -define("editor_tiny/editor",["exports","./loader","core/pending"],(function(_exports,_loader,_pending){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.remove(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);return setupForTarget(target,options)};const getPlugins=options=>options.plugins?options.plugins:defaultOptions.plugins?defaultOptions.plugins:{},getStandardConfig=(target,tinyMCE,options,plugins)=>({target:target,language:document.querySelector("html").lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,toolbar_mode:"sliding",toolbar:[{name:"history",items:["undo","redo"]},{name:"styles",items:["styles"]},{name:"formatting",items:["bold","italic"]},{name:"alignment",items:["alignleft","aligncenter","alignright","alignjustify"]},{name:"indentation",items:["outdent","indent"]},{name:"comments",items:["addcomment"]}],menu:{},plugins:[...plugins],skin:"oxide",promotion:!1}),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]),{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);pluginConfig.forEach((pluginConfig=>{"function"==typeof pluginConfig.configure&&Object.assign(instanceConfig,pluginConfig.configure(instanceConfig))}));const[editor]=await tinyMCE.init(instanceConfig);return instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm)})),editor.moodleOptions=options,pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}})); +define("editor_tiny/editor",["exports","./loader","core/pending"],(function(_exports,_loader,_pending){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.remove(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);return setupForTarget(target,options)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=options=>options.plugins?options.plugins:defaultOptions.plugins?defaultOptions.plugins:{},getStandardConfig=(target,tinyMCE,options,plugins)=>({target:target,language:document.querySelector("html").lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,toolbar_mode:"sliding",toolbar:[{name:"history",items:["undo","redo"]},{name:"styles",items:["styles"]},{name:"formatting",items:["bold","italic"]},{name:"alignment",items:["alignleft","aligncenter","alignright","alignjustify"]},{name:"indentation",items:["outdent","indent"]},{name:"comments",items:["addcomment"]}],menu:{},plugins:[...plugins],skin:"oxide",promotion:!1}),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]),{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);pluginConfig.forEach((pluginConfig=>{"function"==typeof pluginConfig.configure&&Object.assign(instanceConfig,pluginConfig.configure(instanceConfig))}));const[editor]=await tinyMCE.init(instanceConfig);return instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm)})),editor.moodleOptions=options,pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}})); //# sourceMappingURL=editor.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/amd/build/editor.min.js.map b/lib/editor/tiny/amd/build/editor.min.js.map index 354f2f7e5342..99d01e2c6e60 100644 --- a/lib/editor/tiny/amd/build/editor.min.js.map +++ b/lib/editor/tiny/amd/build/editor.min.js.map @@ -1 +1 @@ -{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Utility functions.\n *\n * @module editor_tiny/editor\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {\n getTinyMCE,\n} from './loader';\nimport Pending from 'core/pending';\n\n/**\n * Storage for the TinyMCE instances on the page.\n * @type {Map}\n */\nconst instanceMap = new Map();\n\n/**\n * The default editor configuration.\n * @type {Object}\n */\nlet defaultOptions = {};\n\n/**\n * Require the modules for the named set of TinyMCE plugins.\n *\n * @param {string[]} pluginList The list of plugins\n * @return {Promise[]} A matching set of Promises relating to the requested plugins\n */\nconst importPluginList = async(pluginList) => {\n const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {\n if (pluginPath.indexOf('/') === -1) {\n // A standard TinyMCE Plugin.\n return Promise.resolve(pluginPath);\n }\n\n return import(pluginPath);\n }));\n\n const pluginNames = pluginHandlers.map((pluginConfig) => {\n if (typeof pluginConfig === 'string') {\n return pluginConfig;\n }\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[0];\n }\n return null;\n }).filter((value) => value);\n\n const pluginConfig = pluginHandlers.map((pluginConfig) => {\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[1];\n }\n return null;\n }).filter((value) => value);\n\n return {\n pluginNames,\n pluginConfig,\n };\n};\n\nexport const getAllInstances = () => new Map(instanceMap.entries());\n\n/**\n * Get the TinyMCE instance for the specified Node ID.\n *\n * @param {string} elementId\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));\n\n/*\n * Get the TinyMCE instance for the specified HTMLElement.\n *\n * @param {HTMLElement} element\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElement = element => {\n const instance = instanceMap.get(element);\n if (instance && instance.removed) {\n instanceMap.remove(element);\n return undefined;\n }\n return instance;\n};\n\n/**\n * Set up TinyMCE for the selector at the specified HTML Node id.\n *\n * @param {object} config The configuration required to setup the editor\n * @param {string} config.elementId The HTML Node ID\n * @param {Object} config.options The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForElementId = ({elementId, options}) => {\n const target = document.getElementById(elementId);\n return setupForTarget(target, options);\n};\n\nconst getPlugins = (options) => {\n if (options.plugins) {\n return options.plugins;\n }\n\n if (defaultOptions.plugins) {\n return defaultOptions.plugins;\n }\n\n return {};\n};\n\nconst getStandardConfig = (target, tinyMCE, options, plugins) => {\n const lang = document.querySelector('html').lang;\n return {\n // Set the editor target.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target\n target,\n\n // Set the language.\n // https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language\n language: lang,\n\n // Load the editor stylesheet into the editor iframe.\n // https://www.tiny.cloud/docs/tinymce/6/add-css-options/\n content_css: [\n options.css,\n ],\n\n // Do not convert URLs to relative URLs.\n // https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls\n // eslint-disable-next-line camelcase\n convert_urls: false,\n\n // Enabled 'advanced' a11y options.\n // This includes allowing role=\"presentation\" from the image uploader.\n // https://www.tiny.cloud/docs/tinymce/6/accessibility/\n // eslint-disable-next-line camelcase\n a11y_advanced_options: true,\n\n // Toolbar configuration.\n // https://www.tiny.cloud/docs/tinymce/6/toolbar-configuration-options/\n // TODO: Move this configuration to a passed-in option.\n // eslint-disable-next-line camelcase\n toolbar_mode: 'sliding',\n toolbar: [\n {\n name: 'history',\n items: [\n 'undo',\n 'redo'\n ]\n },\n {\n name: 'styles',\n items: ['styles']\n },\n {\n name: 'formatting',\n items: [\n 'bold',\n 'italic'\n ]\n },\n {\n name: 'alignment',\n items: [\n 'alignleft',\n 'aligncenter',\n 'alignright',\n 'alignjustify'\n ]\n },\n {\n name: 'indentation',\n items: [\n 'outdent',\n 'indent'\n ]\n },\n {\n name: 'comments',\n items: ['addcomment']\n },\n ],\n\n // Menu configuration.\n // https://www.tiny.cloud/docs/tinymce/6/menus-configuration-options/\n // TODO: Move this configuration to a passed-in option.\n menu: {\n },\n\n // The list of plugins to include in the instance.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins\n plugins: [\n ...plugins,\n ],\n\n // TODO Add mobile configuration.\n // Mobile configuration.\n // https://www.tiny.cloud/docs/tinymce/6/tinymce-for-mobile/\n // This will include mobile-specific toolbar, and menu options.\n\n // Skins\n skin: 'oxide',\n\n // Remove the \"Upgrade\" link for Tiny.\n // https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/\n promotion: false,\n };\n};\n\n/**\n * Set up TinyMCE for the HTML Element.\n *\n * @param {HTMLElement} target\n * @param {Object} options The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForTarget = async(target, options = {}) => {\n const instance = getInstanceForElement(target);\n if (instance) {\n return Promise.resolve(instance);\n }\n\n const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');\n\n const plugins = getPlugins(options);\n const [tinyMCE, pluginValues] = await Promise.all([\n getTinyMCE(),\n importPluginList(Object.keys(plugins)),\n ]);\n const {pluginNames, pluginConfig} = pluginValues;\n\n const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);\n pluginConfig.forEach((pluginConfig) => {\n if (typeof pluginConfig.configure === 'function') {\n Object.assign(instanceConfig, pluginConfig.configure(instanceConfig));\n }\n });\n const [editor] = await tinyMCE.init(instanceConfig);\n\n // Store the editor instance in the instanceMap and register its removal to remove it.\n instanceMap.set(target, editor);\n editor.on('remove', ({target}) => {\n // Handle removal of the editor from the map on destruction.\n instanceMap.delete(target.targetElm);\n });\n\n // Store the Moodle-specific options in the TinyMCE instance.\n // TODO: See if there is a more appropriate location for this config.\n // TinyMCE does support custom configuration options in its EditorOptions but these must be registered and spec'd.\n editor.moodleOptions = options;\n\n pendingPromise.resolve();\n return editor;\n};\n\nexport const configureDefaultEditor = (options = {}) => {\n defaultOptions = options;\n};\n"],"names":["instanceMap","Map","defaultOptions","importPluginList","async","pluginHandlers","Promise","all","pluginList","map","pluginPath","indexOf","resolve","pluginNames","pluginConfig","Array","isArray","filter","value","entries","elementId","getInstanceForElement","document","getElementById","element","instance","get","removed","remove","_ref","options","target","setupForTarget","getPlugins","plugins","getStandardConfig","tinyMCE","language","querySelector","lang","content_css","css","convert_urls","a11y_advanced_options","toolbar_mode","toolbar","name","items","menu","skin","promotion","pendingPromise","Pending","pluginValues","Object","keys","instanceConfig","forEach","configure","assign","editor","init","set","on","_ref2","delete","targetElm","moodleOptions"],"mappings":"4iBA+BMA,YAAc,IAAIC,QAMpBC,eAAiB,SAQfC,iBAAmBC,MAAAA,mBACfC,qBAAuBC,QAAQC,IAAIC,WAAWC,KAAIC,aACnB,IAA7BA,WAAWC,QAAQ,KAEZL,QAAQM,QAAQF,4NAGbA,4WAAAA,gBAGZG,YAAcR,eAAeI,KAAKK,cACR,iBAAjBA,aACAA,aAEPC,MAAMC,QAAQF,cACPA,aAAa,GAEjB,OACRG,QAAQC,OAAUA,cASd,CACHL,YAAAA,YACAC,aATiBT,eAAeI,KAAKK,cACjCC,MAAMC,QAAQF,cACPA,aAAa,GAEjB,OACRG,QAAQC,OAAUA,mCAQM,IAAM,IAAIjB,IAAID,YAAYmB,4CAQlBC,WAAaC,sBAAsBC,SAASC,eAAeH,kBAQrFC,sBAAwBG,gBAC3BC,SAAWzB,YAAY0B,IAAIF,aAC7BC,WAAYA,SAASE,eAIlBF,SAHHzB,YAAY4B,OAAOJ,0FAcMK,WAACT,UAACA,UAADU,QAAYA,oBACpCC,OAAST,SAASC,eAAeH,kBAChCY,eAAeD,OAAQD,gBAG5BG,WAAcH,SACZA,QAAQI,QACDJ,QAAQI,QAGfhC,eAAegC,QACRhC,eAAegC,QAGnB,GAGLC,kBAAoB,CAACJ,OAAQK,QAASN,QAASI,WAE1C,CAGHH,OAAAA,OAIAM,SARSf,SAASgB,cAAc,QAAQC,KAYxCC,YAAa,CACTV,QAAQW,KAMZC,cAAc,EAMdC,uBAAuB,EAMvBC,aAAc,UACdC,QAAS,CACL,CACIC,KAAM,UACNC,MAAO,CACH,OACA,SAGR,CACID,KAAM,SACNC,MAAO,CAAC,WAEZ,CACID,KAAM,aACNC,MAAO,CACH,OACA,WAGR,CACID,KAAM,YACNC,MAAO,CACH,YACA,cACA,aACA,iBAGR,CACID,KAAM,cACNC,MAAO,CACH,UACA,WAGR,CACID,KAAM,WACNC,MAAO,CAAC,gBAOhBC,KAAM,GAKNd,QAAS,IACFA,SASPe,KAAM,QAINC,WAAW,IAWNlB,eAAiB5B,eAAM2B,YAAQD,+DAAU,SAC5CL,SAAWJ,sBAAsBU,WACnCN,gBACOnB,QAAQM,QAAQa,gBAGrB0B,eAAiB,IAAIC,iBAAQ,qCAE7BlB,QAAUD,WAAWH,UACpBM,QAASiB,oBAAsB/C,QAAQC,IAAI,EAC9C,wBACAJ,iBAAiBmD,OAAOC,KAAKrB,aAE3BrB,YAACA,YAADC,aAAcA,cAAgBuC,aAE9BG,eAAiBrB,kBAAkBJ,OAAQK,EAASN,QAASjB,aACnEC,aAAa2C,SAAS3C,eACoB,mBAA3BA,aAAa4C,WACpBJ,OAAOK,OAAOH,eAAgB1C,aAAa4C,UAAUF,0BAGtDI,cAAgBxB,QAAQyB,KAAKL,uBAGpCxD,YAAY8D,IAAI/B,OAAQ6B,QACxBA,OAAOG,GAAG,UAAUC,YAACjC,OAACA,cAElB/B,YAAYiE,OAAOlC,OAAOmC,cAM9BN,OAAOO,cAAgBrC,QAEvBqB,eAAevC,UACRgD,+EAG2B,eAAC9B,+DAAU,GAC7C5B,eAAiB4B"} \ No newline at end of file +{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Utility functions.\n *\n * @module editor_tiny/editor\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {\n getTinyMCE,\n} from './loader';\nimport Pending from 'core/pending';\n\n/**\n * Storage for the TinyMCE instances on the page.\n * @type {Map}\n */\nconst instanceMap = new Map();\n\n/**\n * The default editor configuration.\n * @type {Object}\n */\nlet defaultOptions = {};\n\n/**\n * Require the modules for the named set of TinyMCE plugins.\n *\n * @param {string[]} pluginList The list of plugins\n * @return {Promise[]} A matching set of Promises relating to the requested plugins\n */\nconst importPluginList = async(pluginList) => {\n const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {\n if (pluginPath.indexOf('/') === -1) {\n // A standard TinyMCE Plugin.\n return Promise.resolve(pluginPath);\n }\n\n return import(pluginPath);\n }));\n\n const pluginNames = pluginHandlers.map((pluginConfig) => {\n if (typeof pluginConfig === 'string') {\n return pluginConfig;\n }\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[0];\n }\n return null;\n }).filter((value) => value);\n\n const pluginConfig = pluginHandlers.map((pluginConfig) => {\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[1];\n }\n return null;\n }).filter((value) => value);\n\n return {\n pluginNames,\n pluginConfig,\n };\n};\n\nconst fetchLanguage = (language) => fetch(\n `${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}`\n).then(response => response.json());\n\nexport const getAllInstances = () => new Map(instanceMap.entries());\n\n/**\n * Get the TinyMCE instance for the specified Node ID.\n *\n * @param {string} elementId\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));\n\n/*\n * Get the TinyMCE instance for the specified HTMLElement.\n *\n * @param {HTMLElement} element\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElement = element => {\n const instance = instanceMap.get(element);\n if (instance && instance.removed) {\n instanceMap.remove(element);\n return undefined;\n }\n return instance;\n};\n\n/**\n * Set up TinyMCE for the selector at the specified HTML Node id.\n *\n * @param {object} config The configuration required to setup the editor\n * @param {string} config.elementId The HTML Node ID\n * @param {Object} config.options The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForElementId = ({elementId, options}) => {\n const target = document.getElementById(elementId);\n return setupForTarget(target, options);\n};\n\nconst initialisePage = async() => {\n const lang = document.querySelector('html').lang;\n\n const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]);\n tinyMCE.addI18n(lang, langData);\n};\ninitialisePage();\n\nconst getPlugins = (options) => {\n if (options.plugins) {\n return options.plugins;\n }\n\n if (defaultOptions.plugins) {\n return defaultOptions.plugins;\n }\n\n return {};\n};\n\nconst getStandardConfig = (target, tinyMCE, options, plugins) => {\n const lang = document.querySelector('html').lang;\n return {\n // Set the editor target.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target\n target,\n\n // Set the language.\n // https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language\n language: lang,\n\n // Load the editor stylesheet into the editor iframe.\n // https://www.tiny.cloud/docs/tinymce/6/add-css-options/\n content_css: [\n options.css,\n ],\n\n // Do not convert URLs to relative URLs.\n // https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls\n // eslint-disable-next-line camelcase\n convert_urls: false,\n\n // Enabled 'advanced' a11y options.\n // This includes allowing role=\"presentation\" from the image uploader.\n // https://www.tiny.cloud/docs/tinymce/6/accessibility/\n // eslint-disable-next-line camelcase\n a11y_advanced_options: true,\n\n // Toolbar configuration.\n // https://www.tiny.cloud/docs/tinymce/6/toolbar-configuration-options/\n // TODO: Move this configuration to a passed-in option.\n // eslint-disable-next-line camelcase\n toolbar_mode: 'sliding',\n toolbar: [\n {\n name: 'history',\n items: [\n 'undo',\n 'redo'\n ]\n },\n {\n name: 'styles',\n items: ['styles']\n },\n {\n name: 'formatting',\n items: [\n 'bold',\n 'italic'\n ]\n },\n {\n name: 'alignment',\n items: [\n 'alignleft',\n 'aligncenter',\n 'alignright',\n 'alignjustify'\n ]\n },\n {\n name: 'indentation',\n items: [\n 'outdent',\n 'indent'\n ]\n },\n {\n name: 'comments',\n items: ['addcomment']\n },\n ],\n\n // Menu configuration.\n // https://www.tiny.cloud/docs/tinymce/6/menus-configuration-options/\n // TODO: Move this configuration to a passed-in option.\n menu: {\n },\n\n // The list of plugins to include in the instance.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins\n plugins: [\n ...plugins,\n ],\n\n // TODO Add mobile configuration.\n // Mobile configuration.\n // https://www.tiny.cloud/docs/tinymce/6/tinymce-for-mobile/\n // This will include mobile-specific toolbar, and menu options.\n\n // Skins\n skin: 'oxide',\n\n // Remove the \"Upgrade\" link for Tiny.\n // https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/\n promotion: false,\n };\n};\n\n/**\n * Set up TinyMCE for the HTML Element.\n *\n * @param {HTMLElement} target\n * @param {Object} options The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForTarget = async(target, options = {}) => {\n const instance = getInstanceForElement(target);\n if (instance) {\n return Promise.resolve(instance);\n }\n\n const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');\n\n const plugins = getPlugins(options);\n const [tinyMCE, pluginValues] = await Promise.all([\n getTinyMCE(),\n importPluginList(Object.keys(plugins)),\n ]);\n const {pluginNames, pluginConfig} = pluginValues;\n\n const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);\n pluginConfig.forEach((pluginConfig) => {\n if (typeof pluginConfig.configure === 'function') {\n Object.assign(instanceConfig, pluginConfig.configure(instanceConfig));\n }\n });\n const [editor] = await tinyMCE.init(instanceConfig);\n\n // Store the editor instance in the instanceMap and register its removal to remove it.\n instanceMap.set(target, editor);\n editor.on('remove', ({target}) => {\n // Handle removal of the editor from the map on destruction.\n instanceMap.delete(target.targetElm);\n });\n\n // Store the Moodle-specific options in the TinyMCE instance.\n // TODO: See if there is a more appropriate location for this config.\n // TinyMCE does support custom configuration options in its EditorOptions but these must be registered and spec'd.\n editor.moodleOptions = options;\n\n pendingPromise.resolve();\n return editor;\n};\n\nexport const configureDefaultEditor = (options = {}) => {\n defaultOptions = options;\n};\n"],"names":["instanceMap","Map","defaultOptions","importPluginList","async","pluginHandlers","Promise","all","pluginList","map","pluginPath","indexOf","resolve","pluginNames","pluginConfig","Array","isArray","filter","value","entries","elementId","getInstanceForElement","document","getElementById","element","instance","get","removed","remove","_ref","options","target","setupForTarget","lang","querySelector","tinyMCE","langData","language","fetch","M","cfg","wwwroot","langrev","then","response","json","addI18n","initialisePage","getPlugins","plugins","getStandardConfig","content_css","css","convert_urls","a11y_advanced_options","toolbar_mode","toolbar","name","items","menu","skin","promotion","pendingPromise","Pending","pluginValues","Object","keys","instanceConfig","forEach","configure","assign","editor","init","set","on","_ref2","delete","targetElm","moodleOptions"],"mappings":"4iBA+BMA,YAAc,IAAIC,QAMpBC,eAAiB,SAQfC,iBAAmBC,MAAAA,mBACfC,qBAAuBC,QAAQC,IAAIC,WAAWC,KAAIC,aACnB,IAA7BA,WAAWC,QAAQ,KAEZL,QAAQM,QAAQF,4NAGbA,4WAAAA,gBAGZG,YAAcR,eAAeI,KAAKK,cACR,iBAAjBA,aACAA,aAEPC,MAAMC,QAAQF,cACPA,aAAa,GAEjB,OACRG,QAAQC,OAAUA,cASd,CACHL,YAAAA,YACAC,aATiBT,eAAeI,KAAKK,cACjCC,MAAMC,QAAQF,cACPA,aAAa,GAEjB,OACRG,QAAQC,OAAUA,mCAYM,IAAM,IAAIjB,IAAID,YAAYmB,4CAQlBC,WAAaC,sBAAsBC,SAASC,eAAeH,kBAQrFC,sBAAwBG,gBAC3BC,SAAWzB,YAAY0B,IAAIF,aAC7BC,WAAYA,SAASE,eAIlBF,SAHHzB,YAAY4B,OAAOJ,0FAcMK,WAACT,UAACA,UAADU,QAAYA,oBACpCC,OAAST,SAASC,eAAeH,kBAChCY,eAAeD,OAAQD,UAGX1B,iBACb6B,KAAOX,SAASY,cAAc,QAAQD,MAErCE,QAASC,gBAAkB9B,QAAQC,IAAI,EAAC,yBA7C5B8B,SA6CwDJ,KA7C3CK,gBAC7BC,EAAEC,IAAIC,6CAAoCF,EAAEC,IAAIE,oBAAWL,WAChEM,MAAKC,UAAYA,SAASC,YAFLR,IAAAA,SA8CnBF,QAAQW,QAAQb,KAAMG,WAE1BW,SAEMC,WAAclB,SACZA,QAAQmB,QACDnB,QAAQmB,QAGf/C,eAAe+C,QACR/C,eAAe+C,QAGnB,GAGLC,kBAAoB,CAACnB,OAAQI,QAASL,QAASmB,WAE1C,CAGHlB,OAAAA,OAIAM,SARSf,SAASY,cAAc,QAAQD,KAYxCkB,YAAa,CACTrB,QAAQsB,KAMZC,cAAc,EAMdC,uBAAuB,EAMvBC,aAAc,UACdC,QAAS,CACL,CACIC,KAAM,UACNC,MAAO,CACH,OACA,SAGR,CACID,KAAM,SACNC,MAAO,CAAC,WAEZ,CACID,KAAM,aACNC,MAAO,CACH,OACA,WAGR,CACID,KAAM,YACNC,MAAO,CACH,YACA,cACA,aACA,iBAGR,CACID,KAAM,cACNC,MAAO,CACH,UACA,WAGR,CACID,KAAM,WACNC,MAAO,CAAC,gBAOhBC,KAAM,GAKNV,QAAS,IACFA,SASPW,KAAM,QAINC,WAAW,IAWN7B,eAAiB5B,eAAM2B,YAAQD,+DAAU,SAC5CL,SAAWJ,sBAAsBU,WACnCN,gBACOnB,QAAQM,QAAQa,gBAGrBqC,eAAiB,IAAIC,iBAAQ,qCAE7Bd,QAAUD,WAAWlB,UACpBK,QAAS6B,oBAAsB1D,QAAQC,IAAI,EAC9C,wBACAJ,iBAAiB8D,OAAOC,KAAKjB,aAE3BpC,YAACA,YAADC,aAAcA,cAAgBkD,aAE9BG,eAAiBjB,kBAAkBnB,OAAQI,EAASL,QAASjB,aACnEC,aAAasD,SAAStD,eACoB,mBAA3BA,aAAauD,WACpBJ,OAAOK,OAAOH,eAAgBrD,aAAauD,UAAUF,0BAGtDI,cAAgBpC,QAAQqC,KAAKL,uBAGpCnE,YAAYyE,IAAI1C,OAAQwC,QACxBA,OAAOG,GAAG,UAAUC,YAAC5C,OAACA,cAElB/B,YAAY4E,OAAO7C,OAAO8C,cAM9BN,OAAOO,cAAgBhD,QAEvBgC,eAAelD,UACR2D,+EAG2B,eAACzC,+DAAU,GAC7C5B,eAAiB4B"} \ No newline at end of file diff --git a/lib/editor/tiny/amd/src/editor.js b/lib/editor/tiny/amd/src/editor.js index 0cbb9ea8e60f..65044bd23d40 100644 --- a/lib/editor/tiny/amd/src/editor.js +++ b/lib/editor/tiny/amd/src/editor.js @@ -76,6 +76,10 @@ const importPluginList = async(pluginList) => { }; }; +const fetchLanguage = (language) => fetch( + `${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}` +).then(response => response.json()); + export const getAllInstances = () => new Map(instanceMap.entries()); /** @@ -114,6 +118,14 @@ export const setupForElementId = ({elementId, options}) => { return setupForTarget(target, options); }; +const initialisePage = async() => { + const lang = document.querySelector('html').lang; + + const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]); + tinyMCE.addI18n(lang, langData); +}; +initialisePage(); + const getPlugins = (options) => { if (options.plugins) { return options.plugins; diff --git a/lib/editor/tiny/lang.php b/lib/editor/tiny/lang.php new file mode 100644 index 000000000000..0ea6ad2239a7 --- /dev/null +++ b/lib/editor/tiny/lang.php @@ -0,0 +1,347 @@ +. + +/** + * Tiny text editor integration - Language Producer. + * + * @package editor_tiny + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace editor_tiny; + +// Disable moodle specific debug messages and any errors in output, +// comment out when debugging or better look into error log! +define('NO_DEBUG_DISPLAY', true); + +// We need just the values from config.php and minlib.php. +define('ABORT_AFTER_CONFIG', true); + +// This stops immediately at the beginning of lib/setup.php. +require('../../../config.php'); + +/** + * An anonymous class to handle loading and serving lang files for TinyMCE. + * + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class lang { + /** @var string The language code to load */ + protected $lang; + + /** @var int The revision requested */ + protected $rev; + + /** @var bool Whether Moodle is fully loaded or not */ + protected $fullyloaded = false; + + /** + * Constructor to load and serve the langfile. + */ + public function __construct() { + $this->parse_file_information_from_url(); + $this->serve_file(); + } + + /** + * Parse the file information from the URL. + */ + protected function parse_file_information_from_url(): void { + global $CFG; + + // The URL format is /[revision]/[lang]. + // The revision is an integer with negative values meaning the file is not cached. + // The lang is a simple word with no directory separators or special characters. + if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + if (substr_count($slashargument, '/') < 1) { + css_send_css_not_found(); + } + + [$rev, $lang] = explode('/', $slashargument, 2); + $rev = min_clean_param($rev, 'RAW'); + $lang = min_clean_param($lang, 'SAFEDIR'); + } else { + $rev = min_optional_param('rev', 0, 'RAW'); + $lang = min_optional_param('lang', 'standard', 'SAFEDIR'); + } + + $this->lang = $lang; + $this->rev = $rev; + $this->candidatefile = "{$CFG->localcachedir}/editor_tiny/{$this->rev}/lang/{$this->lang}/lang.json"; + } + + /** + * Serve the language pack content. + */ + protected function serve_file(): void { + // Attempt to send the cached langpack. + if ($this->rev > 0) { + if ($this->is_candidate_file_available()) { + // The send_cached_file_if_available function will exit if successful. + // In theory the file could become unavailable after checking that the file exists. + // Whilst this is unlikely, fall back to caching the content below. + $this->send_cached_pack(); + } + + // The file isn't cached yet. + // Load the content. store it in the cache, and serve it. + $strings = $this->load_language_pack(); + $this->store_lang_file($strings); + $this->send_cached(); + } else { + // If the revision is less than 0, then do not cache anything. + $strings = $this->load_language_pack(); + $this->send_uncached($strings); + } + } + + /** + * Load the full Moodle Framework. + */ + protected function load_full_moodle(): void { + global $CFG, $DB, $SESSION, $OUTPUT, $PAGE; + + if ($this->is_full_moodle_loaded()) { + return; + } + + // Ok, now we need to start normal moodle script, we need to load all libs and $DB. + define('ABORT_AFTER_CONFIG_CANCEL', true); + + // Session not used here. + define('NO_MOODLE_COOKIES', true); + + // Ignore upgrade check. + define('NO_UPGRADE_CHECK', true); + + require("{$CFG->dirroot}/lib/setup.php"); + $this->fullyloaded = true; + } + + /** + * Check whether Moodle is fully loaded. + * + * @return bool + */ + public function is_full_moodle_loaded(): bool { + return $this->fullyloaded; + } + + /** + * Load the language pack strings. + * + * @return string[] + */ + protected function load_language_pack(): array { + // We need to load the full moodle API to use the string manager. + $this->load_full_moodle(); + + // We maintain a list of string identifier to original TinyMCE string. + // TinyMCE uses English language strings to perform translations. + $stringlist = file_get_contents(__DIR__ . "/tinystrings.json"); + if (empty($stringlist)) { + $this->send_not_found("Failed to load strings from tinystrings.json"); + } + + $stringlist = json_decode($stringlist, true); + if (empty($stringlist)) { + $this->send_not_found("Failed to load strings from tinystrings.json"); + } + + // Load all strings for the TinyMCE Editor which have a prefix of `tiny:` from the Moodle String Manager. + $stringmanager = get_string_manager(); + $translatedvalues = array_filter( + $stringmanager->load_component_strings('editor_tiny', $this->lang), + function(string $value, string $key): bool { + return strpos($key, 'tiny:') === 0; + }, + ARRAY_FILTER_USE_BOTH + ); + + // We will associate the _original_ TinyMCE string to its translation, but only where it is different. + // Where the original TinyMCE string matches the Moodle translation of it, we do not supply the string. + $strings = []; + foreach ($stringlist as $key => $value) { + if (array_key_exists($key, $translatedvalues)) { + if ($translatedvalues[$key] !== $value) { + $strings[$value] = $translatedvalues[$key]; + } + } + } + + return $strings; + } + + /** + * Send a cached language pack. + */ + protected function send_cached_pack(): void { + global $CFG; + + if (file_exists($this->candidatefile)) { + if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + // We do not actually need to verify the etag value because our files + // never change in cache because we increment the rev counter. + $this->send_unmodified_headers(filemtime($this->candidatefile)); + } + $this->send_cached($this->candidatefile); + } + } + + /** + * Store a langauge cache file containing all of the processed strings. + * + * @param string[] $strings The strings to store + */ + protected function store_lang_file(array $strings): void { + global $CFG; + + clearstatcache(); + if (!file_exists(dirname($this->candidatefile))) { + @mkdir(dirname($this->candidatefile), $CFG->directorypermissions, true); + } + + // Prevent serving of incomplete file from concurrent request, + // the rename() should be more atomic than fwrite(). + ignore_user_abort(true); + + // First up write out the single file for all those using decent browsers. + $content = json_encode($strings, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES); + + $filename = $this->candidatefile; + if ($fp = fopen($filename . '.tmp', 'xb')) { + fwrite($fp, $content); + fclose($fp); + rename($filename . '.tmp', $filename); + @chmod($filename, $CFG->filepermissions); + @unlink($filename . '.tmp'); // Just in case anything fails. + } + + ignore_user_abort(false); + if (connection_aborted()) { + die; + } + } + + /** + * Check whether the candidate file exists. + * + * @return bool + */ + protected function is_candidate_file_available(): bool { + return file_exists($this->candidatefile); + } + + /** + * Get the eTag for the candidate file. + * + * This is a unique hash based on the file arguments. + * It does not need to consider the file content because we use a cache busting URL. + * + * @return string The eTag content + */ + protected function get_etag(): string { + $etag = [ + $this->lang, + $this->rev, + ]; + + return sha1(implode('/', $etag)); + } + + /** + * Send the candidate file, with aggressive cachign headers. + * + * This includdes eTags, a last-modified, and expiry approximately 90 days in the future. + */ + protected function send_cached(): void { + $path = $this->candidatefile; + + // 90 days only - based on Moodle point release cadence being every 3 months. + $lifetime = 60 * 60 * 24 * 90; + + header('Etag: "' . $this->get_etag() . '"'); + header('Content-Disposition: inline; filename="lang.php"'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($path)) . ' GMT'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT'); + header('Pragma: '); + header('Cache-Control: public, max-age=' . $lifetime . ', immutable'); + header('Accept-Ranges: none'); + header('Content-Type: application/json; charset=utf-8'); + if (!min_enable_zlib_compression()) { + header('Content-Length: ' . filesize($path)); + } + + readfile($path); + die; + } + + /** + * Sends the content directly without caching it. + * + * @param string[] $strings + */ + protected function send_uncached(array $strings): void { + header('Content-Disposition: inline; filename="styles_debug.php"'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); + header('Pragma: '); + header('Accept-Ranges: none'); + header('Content-Type: application/json; charset=utf-8'); + + echo json_encode($strings, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES); + die; + } + + /** + * Send file not modified headers. + * + * @param int $lastmodified + */ + protected function send_unmodified_headers($lastmodified): void { + // 90 days only - based on Moodle point release cadence being every 3 months. + $lifetime = 60 * 60 * 24 * 90; + header('HTTP/1.1 304 Not Modified'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT'); + header('Cache-Control: public, max-age=' . $lifetime); + header('Content-Type: application/json; charset=utf-8'); + header('Etag: "' . $this->get_etag() . '"'); + if ($lastmodified) { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastmodified) . ' GMT'); + } + die; + } + + /** + * Sends a 404 message to indicate that the content was not found. + * + * @param null|string $message An optional informative message to include to help debugging + */ + protected function send_not_found(?string $message = null): void { + header('HTTP/1.0 404 not found'); + + if ($message) { + die($message); + } else { + die('Language data was not found, sorry.'); + } + } +}; + +$loader = new lang(); diff --git a/lib/editor/tiny/lang/en/editor_tiny.php b/lib/editor/tiny/lang/en/editor_tiny.php index 67fca5d77b9a..8037339d8e38 100644 --- a/lib/editor/tiny/lang/en/editor_tiny.php +++ b/lib/editor/tiny/lang/en/editor_tiny.php @@ -24,3 +24,407 @@ $string['pluginname'] = 'TinyMCE editor'; $string['privacy:reason'] = 'The TinyMCE Editor does not store any preferences or user data.'; +$string['tiny:hash'] = '#'; +$string['tiny:accessibility'] = 'Accessibility'; +$string['tiny:action'] = 'Action'; +$string['tiny:activity'] = 'Activity'; +$string['tiny:address'] = 'Address'; +$string['tiny:advanced'] = 'Advanced'; +$string['tiny:align'] = 'Align'; +$string['tiny:align_center'] = 'Align center'; +$string['tiny:align_left'] = 'Align left'; +$string['tiny:align_right'] = 'Align right'; +$string['tiny:alignment'] = 'Alignment'; +$string['tiny:all'] = 'All'; +$string['tiny:alternative_description'] = 'Alternative description'; +$string['tiny:alternative_source'] = 'Alternative source'; +$string['tiny:alternative_source_url'] = 'Alternative source URL'; +$string['tiny:anchor'] = 'Anchor'; +$string['tiny:anchor...'] = 'Anchor...'; +$string['tiny:anchors'] = 'Anchors'; +$string['tiny:animals_and_nature'] = 'Animals and Nature'; +$string['tiny:arrows'] = 'Arrows'; +$string['tiny:b'] = 'B'; +$string['tiny:background_color'] = 'Background color'; +$string['tiny:black'] = 'Black'; +$string['tiny:block'] = 'Block'; +$string['tiny:blockquote'] = 'Blockquote'; +$string['tiny:blocks'] = 'Blocks'; +$string['tiny:blue'] = 'Blue'; +$string['tiny:blue_component'] = 'Blue component'; +$string['tiny:body'] = 'Body'; +$string['tiny:bold'] = 'Bold'; +$string['tiny:border'] = 'Border'; +$string['tiny:border_color'] = 'Border color'; +$string['tiny:border_style'] = 'Border style'; +$string['tiny:border_width'] = 'Border width'; +$string['tiny:bottom'] = 'Bottom'; +$string['tiny:browse_for_an_image'] = 'Browse for an image'; +$string['tiny:bullet_list'] = 'Bullet list'; +$string['tiny:cancel'] = 'Cancel'; +$string['tiny:caption'] = 'Caption'; +$string['tiny:cell'] = 'Cell'; +$string['tiny:cell_padding'] = 'Cell padding'; +$string['tiny:cell_properties'] = 'Cell properties'; +$string['tiny:cell_spacing'] = 'Cell spacing'; +$string['tiny:cell_styles'] = 'Cell styles'; +$string['tiny:cell_type'] = 'Cell type'; +$string['tiny:center'] = 'Center'; +$string['tiny:characters'] = 'Characters'; +$string['tiny:characters_no_spaces'] = 'Characters (no spaces)'; +$string['tiny:circle'] = 'Circle'; +$string['tiny:class'] = 'Class'; +$string['tiny:clear_formatting'] = 'Clear formatting'; +$string['tiny:close'] = 'Close'; +$string['tiny:code'] = 'Code'; +$string['tiny:code_sample...'] = 'Code sample...'; +$string['tiny:code_view'] = 'Code view'; +$string['tiny:color_picker'] = 'Color Picker'; +$string['tiny:color_swatch'] = 'Color swatch'; +$string['tiny:cols'] = 'Cols'; +$string['tiny:column'] = 'Column'; +$string['tiny:column_clipboard_actions'] = 'Column clipboard actions'; +$string['tiny:column_group'] = 'Column group'; +$string['tiny:column_header'] = 'Column header'; +$string['tiny:constrain_proportions'] = 'Constrain proportions'; +$string['tiny:copy'] = 'Copy'; +$string['tiny:copy_column'] = 'Copy column'; +$string['tiny:copy_row'] = 'Copy row'; +$string['tiny:could_not_find_the_specified_string.'] = 'Could not find the specified string.'; +$string['tiny:could_not_load_emojis'] = 'Could not load emojis'; +$string['tiny:count'] = 'Count'; +$string['tiny:currency'] = 'Currency'; +$string['tiny:current_window'] = 'Current window'; +$string['tiny:custom_color'] = 'Custom color'; +$string['tiny:custom...'] = 'Custom...'; +$string['tiny:cut'] = 'Cut'; +$string['tiny:cut_column'] = 'Cut column'; +$string['tiny:cut_row'] = 'Cut row'; +$string['tiny:dark_blue'] = 'Dark Blue'; +$string['tiny:dark_gray'] = 'Dark Gray'; +$string['tiny:dark_green'] = 'Dark Green'; +$string['tiny:dark_orange'] = 'Dark Orange'; +$string['tiny:dark_purple'] = 'Dark Purple'; +$string['tiny:dark_red'] = 'Dark Red'; +$string['tiny:dark_turquoise'] = 'Dark Turquoise'; +$string['tiny:dark_yellow'] = 'Dark Yellow'; +$string['tiny:dashed'] = 'Dashed'; +$string['tiny:datetime'] = 'Date/time'; +$string['tiny:decrease_indent'] = 'Decrease indent'; +$string['tiny:default'] = 'Default'; +$string['tiny:delete_column'] = 'Delete column'; +$string['tiny:delete_row'] = 'Delete row'; +$string['tiny:delete_table'] = 'Delete table'; +$string['tiny:dimensions'] = 'Dimensions'; +$string['tiny:disc'] = 'Disc'; +$string['tiny:div'] = 'Div'; +$string['tiny:document'] = 'Document'; +$string['tiny:dotted'] = 'Dotted'; +$string['tiny:double'] = 'Double'; +$string['tiny:drop_an_image_here'] = 'Drop an image here'; +$string['tiny:dropped_file_type_is_not_supported'] = 'Dropped file type is not supported'; +$string['tiny:edit'] = 'Edit'; +$string['tiny:embed'] = 'Embed'; +$string['tiny:emojis'] = 'Emojis'; +$string['tiny:emojis...'] = 'Emojis...'; +$string['tiny:error'] = 'Error'; +$string['tiny:error_form_submit_field_collision.'] = 'Error: Form submit field collision.'; +$string['tiny:error_no_form_element_found.'] = 'Error: No form element found.'; +$string['tiny:extended_latin'] = 'Extended Latin'; +$string['tiny:failed_to_initialize_plugin_0'] = 'Failed to initialize plugin: {0}'; +$string['tiny:failed_to_load_plugin_url_0'] = 'Failed to load plugin url: {0}'; +$string['tiny:failed_to_load_plugin_0_from_url_1'] = 'Failed to load plugin: {0} from url {1}'; +$string['tiny:failed_to_upload_image_0'] = 'Failed to upload image: {0}'; +$string['tiny:file'] = 'File'; +$string['tiny:find'] = 'Find'; +$string['tiny:find_if_searchreplace_plugin_activated'] = 'Find (if searchreplace plugin activated)'; +$string['tiny:find_and_replace'] = 'Find and Replace'; +$string['tiny:find_and_replace...'] = 'Find and replace...'; +$string['tiny:find_in_selection'] = 'Find in selection'; +$string['tiny:find_whole_words_only'] = 'Find whole words only'; +$string['tiny:flags'] = 'Flags'; +$string['tiny:focus_to_contextual_toolbar'] = 'Focus to contextual toolbar'; +$string['tiny:focus_to_element_path'] = 'Focus to element path'; +$string['tiny:focus_to_menubar'] = 'Focus to menubar'; +$string['tiny:focus_to_toolbar'] = 'Focus to toolbar'; +$string['tiny:font'] = 'Font'; +$string['tiny:font_sizes'] = 'Font sizes'; +$string['tiny:fonts'] = 'Fonts'; +$string['tiny:food_and_drink'] = 'Food and Drink'; +$string['tiny:footer'] = 'Footer'; +$string['tiny:format'] = 'Format'; +$string['tiny:formats'] = 'Formats'; +$string['tiny:fullscreen'] = 'Fullscreen'; +$string['tiny:g'] = 'G'; +$string['tiny:general'] = 'General'; +$string['tiny:gray'] = 'Gray'; +$string['tiny:green'] = 'Green'; +$string['tiny:green_component'] = 'Green component'; +$string['tiny:groove'] = 'Groove'; +$string['tiny:handy_shortcuts'] = 'Handy Shortcuts'; +$string['tiny:header'] = 'Header'; +$string['tiny:header_cell'] = 'Header cell'; +$string['tiny:heading_1'] = 'Heading 1'; +$string['tiny:heading_2'] = 'Heading 2'; +$string['tiny:heading_3'] = 'Heading 3'; +$string['tiny:heading_4'] = 'Heading 4'; +$string['tiny:heading_5'] = 'Heading 5'; +$string['tiny:heading_6'] = 'Heading 6'; +$string['tiny:headings'] = 'Headings'; +$string['tiny:height'] = 'Height'; +$string['tiny:help'] = 'Help'; +$string['tiny:hex_color_code'] = 'Hex color code'; +$string['tiny:hidden'] = 'Hidden'; +$string['tiny:horizontal_align'] = 'Horizontal align'; +$string['tiny:horizontal_line'] = 'Horizontal line'; +$string['tiny:horizontal_space'] = 'Horizontal space'; +$string['tiny:id'] = 'ID'; +$string['tiny:id_should_start_with_a_letter_followed_only_by_letters_numbers_dashes_dots_colons_or_underscores.'] = 'ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.'; +$string['tiny:image_is_decorative'] = 'Image is decorative'; +$string['tiny:image_list'] = 'Image list'; +$string['tiny:image_title'] = 'Image title'; +$string['tiny:image...'] = 'Image...'; +$string['tiny:imageproxy_http_error_could_not_find_image_proxy'] = 'ImageProxy HTTP error: Could not find Image Proxy'; +$string['tiny:imageproxy_http_error_incorrect_image_proxy_url'] = 'ImageProxy HTTP error: Incorrect Image Proxy URL'; +$string['tiny:imageproxy_http_error_rejected_request'] = 'ImageProxy HTTP error: Rejected request'; +$string['tiny:imageproxy_http_error_unknown_imageproxy_error'] = 'ImageProxy HTTP error: Unknown ImageProxy error'; +$string['tiny:increase_indent'] = 'Increase indent'; +$string['tiny:inline'] = 'Inline'; +$string['tiny:insert'] = 'Insert'; +$string['tiny:insert_template'] = 'Insert Template'; +$string['tiny:insert_column_after'] = 'Insert column after'; +$string['tiny:insert_column_before'] = 'Insert column before'; +$string['tiny:insert_datetime'] = 'Insert date/time'; +$string['tiny:insert_image'] = 'Insert image'; +$string['tiny:insert_link_if_link_plugin_activated'] = 'Insert link (if link plugin activated)'; +$string['tiny:insert_row_after'] = 'Insert row after'; +$string['tiny:insert_row_before'] = 'Insert row before'; +$string['tiny:insert_table'] = 'Insert table'; +$string['tiny:insert_template...'] = 'Insert template...'; +$string['tiny:insert_video'] = 'Insert video'; +$string['tiny:insertedit_code_sample'] = 'Insert/Edit code sample'; +$string['tiny:insertedit_image'] = 'Insert/edit image'; +$string['tiny:insertedit_link'] = 'Insert/edit link'; +$string['tiny:insertedit_media'] = 'Insert/edit media'; +$string['tiny:insertedit_video'] = 'Insert/edit video'; +$string['tiny:inset'] = 'Inset'; +$string['tiny:invalid_hex_color_code_0'] = 'Invalid hex color code: {0}'; +$string['tiny:invalid_input'] = 'Invalid input'; +$string['tiny:italic'] = 'Italic'; +$string['tiny:justify'] = 'Justify'; +$string['tiny:keyboard_navigation'] = 'Keyboard Navigation'; +$string['tiny:language'] = 'Language'; +$string['tiny:learn_more...'] = 'Learn more...'; +$string['tiny:left'] = 'Left'; +$string['tiny:left_to_right'] = 'Left to right'; +$string['tiny:light_blue'] = 'Light Blue'; +$string['tiny:light_gray'] = 'Light Gray'; +$string['tiny:light_green'] = 'Light Green'; +$string['tiny:light_purple'] = 'Light Purple'; +$string['tiny:light_red'] = 'Light Red'; +$string['tiny:light_yellow'] = 'Light Yellow'; +$string['tiny:line_height'] = 'Line height'; +$string['tiny:link_list'] = 'Link list'; +$string['tiny:link...'] = 'Link...'; +$string['tiny:list_properties'] = 'List Properties'; +$string['tiny:list_properties...'] = 'List properties...'; +$string['tiny:loading_emojis...'] = 'Loading emojis...'; +$string['tiny:loading...'] = 'Loading...'; +$string['tiny:lower_alpha'] = 'Lower Alpha'; +$string['tiny:lower_greek'] = 'Lower Greek'; +$string['tiny:lower_roman'] = 'Lower Roman'; +$string['tiny:match_case'] = 'Match case'; +$string['tiny:mathematical'] = 'Mathematical'; +$string['tiny:media_poster_image_url'] = 'Media poster (Image URL)'; +$string['tiny:media...'] = 'Media...'; +$string['tiny:medium_blue'] = 'Medium Blue'; +$string['tiny:medium_gray'] = 'Medium Gray'; +$string['tiny:medium_purple'] = 'Medium Purple'; +$string['tiny:merge_cells'] = 'Merge cells'; +$string['tiny:middle'] = 'Middle'; +$string['tiny:midnight_blue'] = 'Midnight Blue'; +$string['tiny:more...'] = 'More...'; +$string['tiny:name'] = 'Name'; +$string['tiny:navy_blue'] = 'Navy Blue'; +$string['tiny:new_document'] = 'New document'; +$string['tiny:new_window'] = 'New window'; +$string['tiny:next'] = 'Next'; +$string['tiny:no'] = 'No'; +$string['tiny:no_alignment'] = 'No alignment'; +$string['tiny:no_color'] = 'No color'; +$string['tiny:nonbreaking_space'] = 'Nonbreaking space'; +$string['tiny:none'] = 'None'; +$string['tiny:numbered_list'] = 'Numbered list'; +$string['tiny:or'] = 'OR'; +$string['tiny:objects'] = 'Objects'; +$string['tiny:ok'] = 'Ok'; +$string['tiny:open_help_dialog'] = 'Open help dialog'; +$string['tiny:open_link'] = 'Open link'; +$string['tiny:open_link_in...'] = 'Open link in...'; +$string['tiny:open_popup_menu_for_split_buttons'] = 'Open popup menu for split buttons'; +$string['tiny:orange'] = 'Orange'; +$string['tiny:outset'] = 'Outset'; +$string['tiny:page_break'] = 'Page break'; +$string['tiny:paragraph'] = 'Paragraph'; +$string['tiny:paste'] = 'Paste'; +$string['tiny:paste_as_text'] = 'Paste as text'; +$string['tiny:paste_column_after'] = 'Paste column after'; +$string['tiny:paste_column_before'] = 'Paste column before'; +$string['tiny:paste_is_now_in_plain_text_mode._contents_will_now_be_pasted_as_plain_text_until_you_toggle_this_option_off.'] = 'Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.'; +$string['tiny:paste_or_type_a_link'] = 'Paste or type a link'; +$string['tiny:paste_row_after'] = 'Paste row after'; +$string['tiny:paste_row_before'] = 'Paste row before'; +$string['tiny:paste_your_embed_code_below'] = 'Paste your embed code below:'; +$string['tiny:people'] = 'People'; +$string['tiny:plugins'] = 'Plugins'; +$string['tiny:plugins_installed_0'] = 'Plugins installed ({0}):'; +$string['tiny:powered_by_0'] = 'Powered by {0}'; +$string['tiny:pre'] = 'Pre'; +$string['tiny:preferences'] = 'Preferences'; +$string['tiny:preformatted'] = 'Preformatted'; +$string['tiny:premium_plugins'] = 'Premium plugins:'; +$string['tiny:preview'] = 'Preview'; +$string['tiny:previous'] = 'Previous'; +$string['tiny:print'] = 'Print'; +$string['tiny:print...'] = 'Print...'; +$string['tiny:purple'] = 'Purple'; +$string['tiny:quotations'] = 'Quotations'; +$string['tiny:r'] = 'R'; +$string['tiny:range_0_to_255'] = 'Range 0 to 255'; +$string['tiny:red'] = 'Red'; +$string['tiny:red_component'] = 'Red component'; +$string['tiny:redo'] = 'Redo'; +$string['tiny:remove'] = 'Remove'; +$string['tiny:remove_color'] = 'Remove color'; +$string['tiny:remove_link'] = 'Remove link'; +$string['tiny:replace'] = 'Replace'; +$string['tiny:replace_all'] = 'Replace all'; +$string['tiny:replace_with'] = 'Replace with'; +$string['tiny:resize'] = 'Resize'; +$string['tiny:restore_last_draft'] = 'Restore last draft'; +$string['tiny:rich_text_area'] = 'Rich Text Area'; +$string['tiny:rich_text_area._press_alt-0_for_help.'] = 'Rich Text Area. Press ALT-0 for help.'; +$string['tiny:rich_text_area._press_alt-f9_for_menu._press_alt-f10_for_toolbar._press_alt-0_for_help'] = 'Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help'; +$string['tiny:ridge'] = 'Ridge'; +$string['tiny:right'] = 'Right'; +$string['tiny:right_to_left'] = 'Right to left'; +$string['tiny:row'] = 'Row'; +$string['tiny:row_clipboard_actions'] = 'Row clipboard actions'; +$string['tiny:row_group'] = 'Row group'; +$string['tiny:row_header'] = 'Row header'; +$string['tiny:row_properties'] = 'Row properties'; +$string['tiny:row_type'] = 'Row type'; +$string['tiny:rows'] = 'Rows'; +$string['tiny:save'] = 'Save'; +$string['tiny:save_if_save_plugin_activated'] = 'Save (if save plugin activated)'; +$string['tiny:scope'] = 'Scope'; +$string['tiny:search'] = 'Search'; +$string['tiny:select_all'] = 'Select all'; +$string['tiny:select...'] = 'Select...'; +$string['tiny:selection'] = 'Selection'; +$string['tiny:shortcut'] = 'Shortcut'; +$string['tiny:show_blocks'] = 'Show blocks'; +$string['tiny:show_caption'] = 'Show caption'; +$string['tiny:show_invisible_characters'] = 'Show invisible characters'; +$string['tiny:size'] = 'Size'; +$string['tiny:solid'] = 'Solid'; +$string['tiny:source'] = 'Source'; +$string['tiny:source_code'] = 'Source code'; +$string['tiny:special_character'] = 'Special Character'; +$string['tiny:special_character...'] = 'Special character...'; +$string['tiny:split_cell'] = 'Split cell'; +$string['tiny:square'] = 'Square'; +$string['tiny:start_list_at_number'] = 'Start list at number'; +$string['tiny:strikethrough'] = 'Strikethrough'; +$string['tiny:style'] = 'Style'; +$string['tiny:subscript'] = 'Subscript'; +$string['tiny:superscript'] = 'Superscript'; +$string['tiny:switch_to_or_from_fullscreen_mode'] = 'Switch to or from fullscreen mode'; +$string['tiny:symbols'] = 'Symbols'; +$string['tiny:system_font'] = 'System Font'; +$string['tiny:table'] = 'Table'; +$string['tiny:table_caption'] = 'Table caption'; +$string['tiny:table_properties'] = 'Table properties'; +$string['tiny:table_styles'] = 'Table styles'; +$string['tiny:template'] = 'Template'; +$string['tiny:templates'] = 'Templates'; +$string['tiny:text'] = 'Text'; +$string['tiny:text_color'] = 'Text color'; +$string['tiny:text_to_display'] = 'Text to display'; +$string['tiny:the_url_you_entered_seems_to_be_an_email_address._do_you_want_to_add_the_required_mailto_prefix'] = 'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?'; +$string['tiny:the_url_you_entered_seems_to_be_an_external_link._do_you_want_to_add_the_required_http_prefix'] = 'The URL you entered seems to be an external link. Do you want to add the required http:// prefix?'; +$string['tiny:the_url_you_entered_seems_to_be_an_external_link._do_you_want_to_add_the_required_https_prefix'] = 'The URL you entered seems to be an external link. Do you want to add the required https:// prefix?'; +$string['tiny:title'] = 'Title'; +$string['tiny:to_open_the_popup_press_shiftenter'] = 'To open the popup, press Shift+Enter'; +$string['tiny:tools'] = 'Tools'; +$string['tiny:top'] = 'Top'; +$string['tiny:travel_and_places'] = 'Travel and Places'; +$string['tiny:turquoise'] = 'Turquoise'; +$string['tiny:underline'] = 'Underline'; +$string['tiny:undo'] = 'Undo'; +$string['tiny:upload'] = 'Upload'; +$string['tiny:uploading_image'] = 'Uploading image'; +$string['tiny:upper_alpha'] = 'Upper Alpha'; +$string['tiny:upper_roman'] = 'Upper Roman'; +$string['tiny:url'] = 'Url'; +$string['tiny:user_defined'] = 'User Defined'; +$string['tiny:valid'] = 'Valid'; +$string['tiny:version'] = 'Version'; +$string['tiny:vertical_align'] = 'Vertical align'; +$string['tiny:vertical_space'] = 'Vertical space'; +$string['tiny:view'] = 'View'; +$string['tiny:visual_aids'] = 'Visual aids'; +$string['tiny:warn'] = 'Warn'; +$string['tiny:white'] = 'White'; +$string['tiny:width'] = 'Width'; +$string['tiny:word_count'] = 'Word count'; +$string['tiny:words'] = 'Words'; +$string['tiny:words_0'] = 'Words: {0}'; +$string['tiny:yellow'] = 'Yellow'; +$string['tiny:yes'] = 'Yes'; +$string['tiny:you_are_using_0'] = 'You are using {0}'; +$string['tiny:you_have_unsaved_changes_are_you_sure_you_want_to_navigate_away'] = 'You have unsaved changes are you sure you want to navigate away?'; +$string['tiny:your_browser_doesnt_support_direct_access_to_the_clipboard._please_use_the_ctrlxcv_keyboard_shortcuts_instead.'] = 'Your browser doesn\'t support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.'; +$string['tiny:alignment1'] = 'alignment'; +$string['tiny:austral_sign'] = 'austral sign'; +$string['tiny:cedi_sign'] = 'cedi sign'; +$string['tiny:colon_sign'] = 'colon sign'; +$string['tiny:cruzeiro_sign'] = 'cruzeiro sign'; +$string['tiny:currency_sign'] = 'currency sign'; +$string['tiny:dollar_sign'] = 'dollar sign'; +$string['tiny:dong_sign'] = 'dong sign'; +$string['tiny:drachma_sign'] = 'drachma sign'; +$string['tiny:euro-currency_sign'] = 'euro-currency sign'; +$string['tiny:example'] = 'example'; +$string['tiny:formatting'] = 'formatting'; +$string['tiny:french_franc_sign'] = 'french franc sign'; +$string['tiny:german_penny_symbol'] = 'german penny symbol'; +$string['tiny:guarani_sign'] = 'guarani sign'; +$string['tiny:history'] = 'history'; +$string['tiny:hryvnia_sign'] = 'hryvnia sign'; +$string['tiny:indentation'] = 'indentation'; +$string['tiny:indian_rupee_sign'] = 'indian rupee sign'; +$string['tiny:kip_sign'] = 'kip sign'; +$string['tiny:lira_sign'] = 'lira sign'; +$string['tiny:livre_tournois_sign'] = 'livre tournois sign'; +$string['tiny:manat_sign'] = 'manat sign'; +$string['tiny:mill_sign'] = 'mill sign'; +$string['tiny:naira_sign'] = 'naira sign'; +$string['tiny:new_sheqel_sign'] = 'new sheqel sign'; +$string['tiny:nordic_mark_sign'] = 'nordic mark sign'; +$string['tiny:peseta_sign'] = 'peseta sign'; +$string['tiny:peso_sign'] = 'peso sign'; +$string['tiny:ruble_sign'] = 'ruble sign'; +$string['tiny:rupee_sign'] = 'rupee sign'; +$string['tiny:spesmilo_sign'] = 'spesmilo sign'; +$string['tiny:styles'] = 'styles'; +$string['tiny:tenge_sign'] = 'tenge sign'; +$string['tiny:tugrik_sign'] = 'tugrik sign'; +$string['tiny:turkish_lira_sign'] = 'turkish lira sign'; +$string['tiny:won_sign'] = 'won sign'; +$string['tiny:yen_character'] = 'yen character'; +$string['tiny:yenyuan_character_variant_one'] = 'yen/yuan character variant one'; +$string['tiny:yuan_character'] = 'yuan character'; +$string['tiny:yuan_character_in_hong_kong_and_taiwan'] = 'yuan character, in hong kong and taiwan'; +$string['tiny:0_characters'] = '{0} characters'; +$string['tiny:0_words'] = '{0} words'; diff --git a/lib/editor/tiny/readme_moodle.md b/lib/editor/tiny/readme_moodle.md index 58559e4434d6..07dff6205f53 100644 --- a/lib/editor/tiny/readme_moodle.md +++ b/lib/editor/tiny/readme_moodle.md @@ -40,3 +40,44 @@ ``` 6. Check the (Release notes)[https://www.tiny.cloud/docs/tinymce/6/release-notes/] for any new plugins, premium plugins, menu items, or buttons and add them to classes/manager.php + +## Update procedure for included TinyMCE translations + +1. Visit https://www.tiny.cloud/get-tiny/language-packages/ and download a translation which has been fully translated, for example the German translation. +2. If you did not download the German translation, update the final line of `tools/getOriginals.mjs` to the language code for the relevant translation. +3. Unzip the translation into a new directory: + + ``` + langdir=`mktemp -d` + cd "${langdir}" + unzip path/to/de.zip + ``` + +4. Run the translation tool: + + ``` + node "${MOODLEDIR}/tools/getOriginals.mjs" + ``` + + This will generate two files + +5. Copy the `tinystrings.json` file into the Moodle directory + + ``` + cp tinystrings.json "${MOODLEDIR}/tinystrings.json" + ``` + +6. Copy the content of the `strings.php` file over the existing tiny strings: + + ``` + sed -i "/string\['tiny:/d" "${MOODLEDIR}/lang/en/editor_tiny.php" + cat strings.php >> "${MOODLEDIR}/lang/en/editor_tiny.php" + ``` + +7. Commit changes + +--- + +**Note:** You will need to manually check for any Moodle-updated language strings as part of this change (for example any from the en_fixes). + +--- diff --git a/lib/editor/tiny/tinystrings.json b/lib/editor/tiny/tinystrings.json new file mode 100644 index 000000000000..88b54fac2f58 --- /dev/null +++ b/lib/editor/tiny/tinystrings.json @@ -0,0 +1,406 @@ +{ + "tiny:hash": "#", + "tiny:accessibility": "Accessibility", + "tiny:action": "Action", + "tiny:activity": "Activity", + "tiny:address": "Address", + "tiny:advanced": "Advanced", + "tiny:align": "Align", + "tiny:align_center": "Align center", + "tiny:align_left": "Align left", + "tiny:align_right": "Align right", + "tiny:alignment": "Alignment", + "tiny:all": "All", + "tiny:alternative_description": "Alternative description", + "tiny:alternative_source": "Alternative source", + "tiny:alternative_source_url": "Alternative source URL", + "tiny:anchor": "Anchor", + "tiny:anchor...": "Anchor...", + "tiny:anchors": "Anchors", + "tiny:animals_and_nature": "Animals and Nature", + "tiny:arrows": "Arrows", + "tiny:b": "B", + "tiny:background_color": "Background color", + "tiny:black": "Black", + "tiny:block": "Block", + "tiny:blockquote": "Blockquote", + "tiny:blocks": "Blocks", + "tiny:blue": "Blue", + "tiny:blue_component": "Blue component", + "tiny:body": "Body", + "tiny:bold": "Bold", + "tiny:border": "Border", + "tiny:border_color": "Border color", + "tiny:border_style": "Border style", + "tiny:border_width": "Border width", + "tiny:bottom": "Bottom", + "tiny:browse_for_an_image": "Browse for an image", + "tiny:bullet_list": "Bullet list", + "tiny:cancel": "Cancel", + "tiny:caption": "Caption", + "tiny:cell": "Cell", + "tiny:cell_padding": "Cell padding", + "tiny:cell_properties": "Cell properties", + "tiny:cell_spacing": "Cell spacing", + "tiny:cell_styles": "Cell styles", + "tiny:cell_type": "Cell type", + "tiny:center": "Center", + "tiny:characters": "Characters", + "tiny:characters_no_spaces": "Characters (no spaces)", + "tiny:circle": "Circle", + "tiny:class": "Class", + "tiny:clear_formatting": "Clear formatting", + "tiny:close": "Close", + "tiny:code": "Code", + "tiny:code_sample...": "Code sample...", + "tiny:code_view": "Code view", + "tiny:color_picker": "Color Picker", + "tiny:color_swatch": "Color swatch", + "tiny:cols": "Cols", + "tiny:column": "Column", + "tiny:column_clipboard_actions": "Column clipboard actions", + "tiny:column_group": "Column group", + "tiny:column_header": "Column header", + "tiny:constrain_proportions": "Constrain proportions", + "tiny:copy": "Copy", + "tiny:copy_column": "Copy column", + "tiny:copy_row": "Copy row", + "tiny:could_not_find_the_specified_string.": "Could not find the specified string.", + "tiny:could_not_load_emojis": "Could not load emojis", + "tiny:count": "Count", + "tiny:currency": "Currency", + "tiny:current_window": "Current window", + "tiny:custom_color": "Custom color", + "tiny:custom...": "Custom...", + "tiny:cut": "Cut", + "tiny:cut_column": "Cut column", + "tiny:cut_row": "Cut row", + "tiny:dark_blue": "Dark Blue", + "tiny:dark_gray": "Dark Gray", + "tiny:dark_green": "Dark Green", + "tiny:dark_orange": "Dark Orange", + "tiny:dark_purple": "Dark Purple", + "tiny:dark_red": "Dark Red", + "tiny:dark_turquoise": "Dark Turquoise", + "tiny:dark_yellow": "Dark Yellow", + "tiny:dashed": "Dashed", + "tiny:datetime": "Date/time", + "tiny:decrease_indent": "Decrease indent", + "tiny:default": "Default", + "tiny:delete_column": "Delete column", + "tiny:delete_row": "Delete row", + "tiny:delete_table": "Delete table", + "tiny:dimensions": "Dimensions", + "tiny:disc": "Disc", + "tiny:div": "Div", + "tiny:document": "Document", + "tiny:dotted": "Dotted", + "tiny:double": "Double", + "tiny:drop_an_image_here": "Drop an image here", + "tiny:dropped_file_type_is_not_supported": "Dropped file type is not supported", + "tiny:edit": "Edit", + "tiny:embed": "Embed", + "tiny:emojis": "Emojis", + "tiny:emojis...": "Emojis...", + "tiny:error": "Error", + "tiny:error_form_submit_field_collision.": "Error: Form submit field collision.", + "tiny:error_no_form_element_found.": "Error: No form element found.", + "tiny:extended_latin": "Extended Latin", + "tiny:failed_to_initialize_plugin_0": "Failed to initialize plugin: {0}", + "tiny:failed_to_load_plugin_url_0": "Failed to load plugin url: {0}", + "tiny:failed_to_load_plugin_0_from_url_1": "Failed to load plugin: {0} from url {1}", + "tiny:failed_to_upload_image_0": "Failed to upload image: {0}", + "tiny:file": "File", + "tiny:find": "Find", + "tiny:find_if_searchreplace_plugin_activated": "Find (if searchreplace plugin activated)", + "tiny:find_and_replace": "Find and Replace", + "tiny:find_and_replace...": "Find and replace...", + "tiny:find_in_selection": "Find in selection", + "tiny:find_whole_words_only": "Find whole words only", + "tiny:flags": "Flags", + "tiny:focus_to_contextual_toolbar": "Focus to contextual toolbar", + "tiny:focus_to_element_path": "Focus to element path", + "tiny:focus_to_menubar": "Focus to menubar", + "tiny:focus_to_toolbar": "Focus to toolbar", + "tiny:font": "Font", + "tiny:font_sizes": "Font sizes", + "tiny:fonts": "Fonts", + "tiny:food_and_drink": "Food and Drink", + "tiny:footer": "Footer", + "tiny:format": "Format", + "tiny:formats": "Formats", + "tiny:fullscreen": "Fullscreen", + "tiny:g": "G", + "tiny:general": "General", + "tiny:gray": "Gray", + "tiny:green": "Green", + "tiny:green_component": "Green component", + "tiny:groove": "Groove", + "tiny:handy_shortcuts": "Handy Shortcuts", + "tiny:header": "Header", + "tiny:header_cell": "Header cell", + "tiny:heading_1": "Heading 1", + "tiny:heading_2": "Heading 2", + "tiny:heading_3": "Heading 3", + "tiny:heading_4": "Heading 4", + "tiny:heading_5": "Heading 5", + "tiny:heading_6": "Heading 6", + "tiny:headings": "Headings", + "tiny:height": "Height", + "tiny:help": "Help", + "tiny:hex_color_code": "Hex color code", + "tiny:hidden": "Hidden", + "tiny:horizontal_align": "Horizontal align", + "tiny:horizontal_line": "Horizontal line", + "tiny:horizontal_space": "Horizontal space", + "tiny:id": "ID", + "tiny:id_should_start_with_a_letter_followed_only_by_letters_numbers_dashes_dots_colons_or_underscores.": "ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.", + "tiny:image_is_decorative": "Image is decorative", + "tiny:image_list": "Image list", + "tiny:image_title": "Image title", + "tiny:image...": "Image...", + "tiny:imageproxy_http_error_could_not_find_image_proxy": "ImageProxy HTTP error: Could not find Image Proxy", + "tiny:imageproxy_http_error_incorrect_image_proxy_url": "ImageProxy HTTP error: Incorrect Image Proxy URL", + "tiny:imageproxy_http_error_rejected_request": "ImageProxy HTTP error: Rejected request", + "tiny:imageproxy_http_error_unknown_imageproxy_error": "ImageProxy HTTP error: Unknown ImageProxy error", + "tiny:increase_indent": "Increase indent", + "tiny:inline": "Inline", + "tiny:insert": "Insert", + "tiny:insert_template": "Insert Template", + "tiny:insert_column_after": "Insert column after", + "tiny:insert_column_before": "Insert column before", + "tiny:insert_datetime": "Insert date/time", + "tiny:insert_image": "Insert image", + "tiny:insert_link_if_link_plugin_activated": "Insert link (if link plugin activated)", + "tiny:insert_row_after": "Insert row after", + "tiny:insert_row_before": "Insert row before", + "tiny:insert_table": "Insert table", + "tiny:insert_template...": "Insert template...", + "tiny:insert_video": "Insert video", + "tiny:insertedit_code_sample": "Insert/Edit code sample", + "tiny:insertedit_image": "Insert/edit image", + "tiny:insertedit_link": "Insert/edit link", + "tiny:insertedit_media": "Insert/edit media", + "tiny:insertedit_video": "Insert/edit video", + "tiny:inset": "Inset", + "tiny:invalid_hex_color_code_0": "Invalid hex color code: {0}", + "tiny:invalid_input": "Invalid input", + "tiny:italic": "Italic", + "tiny:justify": "Justify", + "tiny:keyboard_navigation": "Keyboard Navigation", + "tiny:language": "Language", + "tiny:learn_more...": "Learn more...", + "tiny:left": "Left", + "tiny:left_to_right": "Left to right", + "tiny:light_blue": "Light Blue", + "tiny:light_gray": "Light Gray", + "tiny:light_green": "Light Green", + "tiny:light_purple": "Light Purple", + "tiny:light_red": "Light Red", + "tiny:light_yellow": "Light Yellow", + "tiny:line_height": "Line height", + "tiny:link_list": "Link list", + "tiny:link...": "Link...", + "tiny:list_properties": "List Properties", + "tiny:list_properties...": "List properties...", + "tiny:loading_emojis...": "Loading emojis...", + "tiny:loading...": "Loading...", + "tiny:lower_alpha": "Lower Alpha", + "tiny:lower_greek": "Lower Greek", + "tiny:lower_roman": "Lower Roman", + "tiny:match_case": "Match case", + "tiny:mathematical": "Mathematical", + "tiny:media_poster_image_url": "Media poster (Image URL)", + "tiny:media...": "Media...", + "tiny:medium_blue": "Medium Blue", + "tiny:medium_gray": "Medium Gray", + "tiny:medium_purple": "Medium Purple", + "tiny:merge_cells": "Merge cells", + "tiny:middle": "Middle", + "tiny:midnight_blue": "Midnight Blue", + "tiny:more...": "More...", + "tiny:name": "Name", + "tiny:navy_blue": "Navy Blue", + "tiny:new_document": "New document", + "tiny:new_window": "New window", + "tiny:next": "Next", + "tiny:no": "No", + "tiny:no_alignment": "No alignment", + "tiny:no_color": "No color", + "tiny:nonbreaking_space": "Nonbreaking space", + "tiny:none": "None", + "tiny:numbered_list": "Numbered list", + "tiny:or": "OR", + "tiny:objects": "Objects", + "tiny:ok": "Ok", + "tiny:open_help_dialog": "Open help dialog", + "tiny:open_link": "Open link", + "tiny:open_link_in...": "Open link in...", + "tiny:open_popup_menu_for_split_buttons": "Open popup menu for split buttons", + "tiny:orange": "Orange", + "tiny:outset": "Outset", + "tiny:page_break": "Page break", + "tiny:paragraph": "Paragraph", + "tiny:paste": "Paste", + "tiny:paste_as_text": "Paste as text", + "tiny:paste_column_after": "Paste column after", + "tiny:paste_column_before": "Paste column before", + "tiny:paste_is_now_in_plain_text_mode._contents_will_now_be_pasted_as_plain_text_until_you_toggle_this_option_off.": "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.", + "tiny:paste_or_type_a_link": "Paste or type a link", + "tiny:paste_row_after": "Paste row after", + "tiny:paste_row_before": "Paste row before", + "tiny:paste_your_embed_code_below": "Paste your embed code below:", + "tiny:people": "People", + "tiny:plugins": "Plugins", + "tiny:plugins_installed_0": "Plugins installed ({0}):", + "tiny:powered_by_0": "Powered by {0}", + "tiny:pre": "Pre", + "tiny:preferences": "Preferences", + "tiny:preformatted": "Preformatted", + "tiny:premium_plugins": "Premium plugins:", + "tiny:preview": "Preview", + "tiny:previous": "Previous", + "tiny:print": "Print", + "tiny:print...": "Print...", + "tiny:purple": "Purple", + "tiny:quotations": "Quotations", + "tiny:r": "R", + "tiny:range_0_to_255": "Range 0 to 255", + "tiny:red": "Red", + "tiny:red_component": "Red component", + "tiny:redo": "Redo", + "tiny:remove": "Remove", + "tiny:remove_color": "Remove color", + "tiny:remove_link": "Remove link", + "tiny:replace": "Replace", + "tiny:replace_all": "Replace all", + "tiny:replace_with": "Replace with", + "tiny:resize": "Resize", + "tiny:restore_last_draft": "Restore last draft", + "tiny:rich_text_area": "Rich Text Area", + "tiny:rich_text_area._press_alt-0_for_help.": "Rich Text Area. Press ALT-0 for help.", + "tiny:rich_text_area._press_alt-f9_for_menu._press_alt-f10_for_toolbar._press_alt-0_for_help": "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help", + "tiny:ridge": "Ridge", + "tiny:right": "Right", + "tiny:right_to_left": "Right to left", + "tiny:row": "Row", + "tiny:row_clipboard_actions": "Row clipboard actions", + "tiny:row_group": "Row group", + "tiny:row_header": "Row header", + "tiny:row_properties": "Row properties", + "tiny:row_type": "Row type", + "tiny:rows": "Rows", + "tiny:save": "Save", + "tiny:save_if_save_plugin_activated": "Save (if save plugin activated)", + "tiny:scope": "Scope", + "tiny:search": "Search", + "tiny:select_all": "Select all", + "tiny:select...": "Select...", + "tiny:selection": "Selection", + "tiny:shortcut": "Shortcut", + "tiny:show_blocks": "Show blocks", + "tiny:show_caption": "Show caption", + "tiny:show_invisible_characters": "Show invisible characters", + "tiny:size": "Size", + "tiny:solid": "Solid", + "tiny:source": "Source", + "tiny:source_code": "Source code", + "tiny:special_character": "Special Character", + "tiny:special_character...": "Special character...", + "tiny:split_cell": "Split cell", + "tiny:square": "Square", + "tiny:start_list_at_number": "Start list at number", + "tiny:strikethrough": "Strikethrough", + "tiny:style": "Style", + "tiny:subscript": "Subscript", + "tiny:superscript": "Superscript", + "tiny:switch_to_or_from_fullscreen_mode": "Switch to or from fullscreen mode", + "tiny:symbols": "Symbols", + "tiny:system_font": "System Font", + "tiny:table": "Table", + "tiny:table_caption": "Table caption", + "tiny:table_properties": "Table properties", + "tiny:table_styles": "Table styles", + "tiny:template": "Template", + "tiny:templates": "Templates", + "tiny:text": "Text", + "tiny:text_color": "Text color", + "tiny:text_to_display": "Text to display", + "tiny:the_url_you_entered_seems_to_be_an_email_address._do_you_want_to_add_the_required_mailto_prefix": "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?", + "tiny:the_url_you_entered_seems_to_be_an_external_link._do_you_want_to_add_the_required_http_prefix": "The URL you entered seems to be an external link. Do you want to add the required http:// prefix?", + "tiny:the_url_you_entered_seems_to_be_an_external_link._do_you_want_to_add_the_required_https_prefix": "The URL you entered seems to be an external link. Do you want to add the required https:// prefix?", + "tiny:title": "Title", + "tiny:to_open_the_popup_press_shiftenter": "To open the popup, press Shift+Enter", + "tiny:tools": "Tools", + "tiny:top": "Top", + "tiny:travel_and_places": "Travel and Places", + "tiny:turquoise": "Turquoise", + "tiny:underline": "Underline", + "tiny:undo": "Undo", + "tiny:upload": "Upload", + "tiny:uploading_image": "Uploading image", + "tiny:upper_alpha": "Upper Alpha", + "tiny:upper_roman": "Upper Roman", + "tiny:url": "Url", + "tiny:user_defined": "User Defined", + "tiny:valid": "Valid", + "tiny:version": "Version", + "tiny:vertical_align": "Vertical align", + "tiny:vertical_space": "Vertical space", + "tiny:view": "View", + "tiny:visual_aids": "Visual aids", + "tiny:warn": "Warn", + "tiny:white": "White", + "tiny:width": "Width", + "tiny:word_count": "Word count", + "tiny:words": "Words", + "tiny:words_0": "Words: {0}", + "tiny:yellow": "Yellow", + "tiny:yes": "Yes", + "tiny:you_are_using_0": "You are using {0}", + "tiny:you_have_unsaved_changes_are_you_sure_you_want_to_navigate_away": "You have unsaved changes are you sure you want to navigate away?", + "tiny:your_browser_doesnt_support_direct_access_to_the_clipboard._please_use_the_ctrlxcv_keyboard_shortcuts_instead.": "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.", + "tiny:alignment1": "alignment", + "tiny:austral_sign": "austral sign", + "tiny:cedi_sign": "cedi sign", + "tiny:colon_sign": "colon sign", + "tiny:cruzeiro_sign": "cruzeiro sign", + "tiny:currency_sign": "currency sign", + "tiny:dollar_sign": "dollar sign", + "tiny:dong_sign": "dong sign", + "tiny:drachma_sign": "drachma sign", + "tiny:euro-currency_sign": "euro-currency sign", + "tiny:example": "example", + "tiny:formatting": "formatting", + "tiny:french_franc_sign": "french franc sign", + "tiny:german_penny_symbol": "german penny symbol", + "tiny:guarani_sign": "guarani sign", + "tiny:history": "history", + "tiny:hryvnia_sign": "hryvnia sign", + "tiny:indentation": "indentation", + "tiny:indian_rupee_sign": "indian rupee sign", + "tiny:kip_sign": "kip sign", + "tiny:lira_sign": "lira sign", + "tiny:livre_tournois_sign": "livre tournois sign", + "tiny:manat_sign": "manat sign", + "tiny:mill_sign": "mill sign", + "tiny:naira_sign": "naira sign", + "tiny:new_sheqel_sign": "new sheqel sign", + "tiny:nordic_mark_sign": "nordic mark sign", + "tiny:peseta_sign": "peseta sign", + "tiny:peso_sign": "peso sign", + "tiny:ruble_sign": "ruble sign", + "tiny:rupee_sign": "rupee sign", + "tiny:spesmilo_sign": "spesmilo sign", + "tiny:styles": "styles", + "tiny:tenge_sign": "tenge sign", + "tiny:tugrik_sign": "tugrik sign", + "tiny:turkish_lira_sign": "turkish lira sign", + "tiny:won_sign": "won sign", + "tiny:yen_character": "yen character", + "tiny:yenyuan_character_variant_one": "yen/yuan character variant one", + "tiny:yuan_character": "yuan character", + "tiny:yuan_character_in_hong_kong_and_taiwan": "yuan character, in hong kong and taiwan", + "tiny:0_characters": "{0} characters", + "tiny:0_words": "{0} words" +} \ No newline at end of file diff --git a/lib/editor/tiny/tools/getOriginals.mjs b/lib/editor/tiny/tools/getOriginals.mjs new file mode 100644 index 000000000000..e2a028b5a8c6 --- /dev/null +++ b/lib/editor/tiny/tools/getOriginals.mjs @@ -0,0 +1,86 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +import {readFile, writeFile} from 'fs/promises'; + +const readStringsFromLanguages = async (language) => { + const fileContent = await readFile(`./langs/${language}.js`, 'utf-8'); + + const translations = []; + const tinymce = { + addI18n: (language, strings) => { + translations.push(...(Object.keys(strings))); + }, + }; + + eval(fileContent); + + return translations.sort(); +}; + +const getStringMap = (strings) => { + const stringMap = {}; + + const getUniqueKeyForString = (string, modifier = 0) => { + let stringKey = string.toLowerCase() + .replaceAll(' ', '_') + .replaceAll(/\{(\d)\}/g, '$1') + .replaceAll('#', 'hash') + .replaceAll(/[^a-z0-9_\-\.]/g, '') + ; + + if (stringKey === '') { + throw new Error(`The calculated key for '${string}' was empty`); + } + + stringKey = `tiny:${stringKey}`; + + if (modifier > 0) { + stringKey = `${stringKey}${modifier}`; + } + + if (typeof stringMap[stringKey] !== 'undefined') { + return getUniqueKeyForString(string, ++modifier); + } + + return stringKey; + }; + + strings.forEach((string) => { + const stringKey = getUniqueKeyForString(string); + if (typeof stringMap[stringKey] !== 'undefined') { + throw new Error(`Found existing key ${stringKey}`); + } + + stringMap[stringKey] = string; + }); + + return stringMap; +}; + +const getPhpStrings = (stringMap) => Object.entries(stringMap).map(([stringKey, stringValue]) => { + return `$string['${stringKey}'] = '${stringValue.replace("'", "\\\'")}';` +}).join("\n"); + + +const constructTranslationFile = async(language) => { + const strings = await readStringsFromLanguages(language); + const stringMap = getStringMap(strings); + + await writeFile('./strings.php', getPhpStrings(stringMap) + "\n"); + await writeFile('./tinystrings.json', JSON.stringify(stringMap, null, ' ')); +}; + +constructTranslationFile('de');