diff --git a/lib/editor/tiny/lang/en/editor_tiny.php b/lib/editor/tiny/lang/en/editor_tiny.php index 0767a54a025..723035aa213 100644 --- a/lib/editor/tiny/lang/en/editor_tiny.php +++ b/lib/editor/tiny/lang/en/editor_tiny.php @@ -183,7 +183,7 @@ $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:id_should_start_with_a_letter_followed_only_by_letters_numbers_dashes_dots_colons_or_'] = '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'; @@ -274,7 +274,7 @@ $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_is_now_in_plain_text_mode._contents_will_now_be_pasted_as_plain_text_until_you_'] = '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'; @@ -308,7 +308,7 @@ $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:rich_text_area._press_alt-f9_for_menu._press_alt-f10_for_toolbar._press_alt-0_for_hel'] = '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'; @@ -355,9 +355,9 @@ $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:the_url_you_entered_seems_to_be_an_email_address._do_you_want_to_add_the_required_mai'] = '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_htt'] = '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_ht1'] = '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'; @@ -388,7 +388,7 @@ $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:your_browser_doesnt_support_direct_access_to_the_clipboard._please_use_the_ctrlxcv_ke'] = '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'; diff --git a/lib/editor/tiny/readme_moodle.md b/lib/editor/tiny/readme_moodle.md index 07dff6205f5..72982c89cc5 100644 --- a/lib/editor/tiny/readme_moodle.md +++ b/lib/editor/tiny/readme_moodle.md @@ -43,23 +43,23 @@ ## 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. +1. Visit https://www.tiny.cloud/get-tiny/language-packages/ and download the "TinyMCE 6 All languages" zip file. +2. Check the list of languages and confirm that the German translation is still at 100%. If not, then make a note of a language which is. 3. Unzip the translation into a new directory: - ``` + ```bash langdir=`mktemp -d` cd "${langdir}" - unzip path/to/de.zip + unzip path/to/langs.zip ``` 4. Run the translation tool: - ``` - node "${MOODLEDIR}/tools/getOriginals.mjs" + ```bash + node "${MOODLEDIR}/tools/createLangStrings.mjs" ``` - This will generate two files + This will generate a language file for each available Language, as well as a `tinystrings.json`, and a `strings.php` which will be used in the subsequent steps. 5. Copy the `tinystrings.json` file into the Moodle directory @@ -75,9 +75,12 @@ ``` 7. Commit changes +8. If required, the remaining language strings can be fed into AMOS. --- +**Note:** A set of language files are also generated for all supported translations and may be submitted to AMOS if desired. + **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 index 88b54fac2f5..c0ca466ac6f 100644 --- a/lib/editor/tiny/tinystrings.json +++ b/lib/editor/tiny/tinystrings.json @@ -154,7 +154,7 @@ "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:id_should_start_with_a_letter_followed_only_by_letters_numbers_dashes_dots_colons_or_": "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", @@ -245,7 +245,7 @@ "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_is_now_in_plain_text_mode._contents_will_now_be_pasted_as_plain_text_until_you_": "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", @@ -279,7 +279,7 @@ "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:rich_text_area._press_alt-f9_for_menu._press_alt-f10_for_toolbar._press_alt-0_for_hel": "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", @@ -326,9 +326,9 @@ "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:the_url_you_entered_seems_to_be_an_email_address._do_you_want_to_add_the_required_mai": "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_htt": "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_ht1": "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", @@ -359,7 +359,7 @@ "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:your_browser_doesnt_support_direct_access_to_the_clipboard._please_use_the_ctrlxcv_ke": "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", diff --git a/lib/editor/tiny/tools/createLangStrings.mjs b/lib/editor/tiny/tools/createLangStrings.mjs new file mode 100644 index 00000000000..d0f747dd077 --- /dev/null +++ b/lib/editor/tiny/tools/createLangStrings.mjs @@ -0,0 +1,139 @@ +// 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 {readdir, readFile, writeFile, mkdir} from 'fs/promises'; +import {join as joinPath} from 'path'; + +const fullyTranslatedLanguage = 'de'; +const maxStringIdentifierLength = 90; + +const readStringsFromLanguages = async (language) => { + const fileContent = await readFile(`./langs/${language}.js`, 'utf-8'); + + let translations = {}; + const tinymce = { + addI18n: (language, strings) => { + translations = strings; + }, + }; + + eval(fileContent); + + return Object.keys(translations).sort().reduce((sortedTranslations, key) => { + sortedTranslations[key] = translations[key]; + return sortedTranslations; + }, {}); +}; + +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 (stringKey.length > maxStringIdentifierLength) { + const modifierLength = modifier === 0 ? 0 : `${modifier}`.length; + stringKey = stringKey.slice(0, maxStringIdentifierLength - modifierLength); + } + + 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, translatedStrings) => Object.entries(stringMap).map(([stringKey, englishString]) => { + if (translatedStrings[englishString].length === 0) { + return null; + } + return `$string['${stringKey}'] = '${translatedStrings[englishString].replaceAll("'", "\\\'")}';` +}) +.filter((value) => value !== null) +.join("\n"); + +const storeEnglishStrings = async(stringMap) => { + const englishStrings = Object.entries(stringMap).map(([stringKey, stringValue]) => { + return `$string['${stringKey}'] = '${stringValue.replace("'", "\\\'")}';` + }).join("\n"); + await writeFile('./strings.php', englishStrings + "\n"); + await writeFile('./tinystrings.json', JSON.stringify(stringMap, null, ' ')); +} + +const constructTranslationFile = async(language, englishStringMap = null) => { + const strings = await readStringsFromLanguages(language); + console.log(`Generating translation data for ${language} with ${Object.keys(strings).length} strings`); + const stringMap = englishStringMap === null ? getStringMap(Object.keys(strings)) : englishStringMap; + + const langDir = joinPath('lang', language); + await mkdir(langDir, {recursive: true}); + + const fileContent = ` { + const {stringMap} = await constructTranslationFile(fullyTranslatedLanguage); + storeEnglishStrings(stringMap); + + readdir('./langs/').then((files) => { + files.forEach(async(file) => { + const langIdent = file.replace('.js', ''); + if (langIdent === fullyTranslatedLanguage) { + // This language is already done. + return; + } + await constructTranslationFile(langIdent, stringMap); + }); + }); +} + +constructTranslationFiles(); diff --git a/lib/editor/tiny/tools/getOriginals.mjs b/lib/editor/tiny/tools/getOriginals.mjs deleted file mode 100644 index e2a028b5a8c..00000000000 --- a/lib/editor/tiny/tools/getOriginals.mjs +++ /dev/null @@ -1,86 +0,0 @@ -// 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');