From 7dab4d3640b7b37c407eea111eda1fc0b71adbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Smodi=C5=A1?= Date: Mon, 28 Sep 2015 18:44:48 +0200 Subject: Implement in-structure GUI translation for python and problem editing screens. Hints are not yet covered. --- js/codeq/comms.js | 8 ++- js/codeq/hint.js | 23 ++++++- js/codeq/navigation.js | 2 +- js/codeq/problem.js | 173 ++++++++++++++++++++++++++++++++++++++++++++---- js/codeq/prolog.js | 33 +++++---- js/codeq/python.js | 33 +++++---- js/codeq/translation.js | 7 ++ 7 files changed, 236 insertions(+), 43 deletions(-) (limited to 'js') diff --git a/js/codeq/comms.js b/js/codeq/comms.js index 2d934bd..5ac3d66 100644 --- a/js/codeq/comms.js +++ b/js/codeq/comms.js @@ -164,8 +164,8 @@ // AJAX communication support functions (loading of static web resources) // ================================================================================ - var languageCache = {},// language defs, keyed by language identifier - problemCache = {},// problem cache, 3-level, keyed by: language, problem group, and problem identifier + var languageCache = {}, // language defs, keyed by language identifier + problemCache = {}, // problem cache, 3-level, keyed by: language, problem group, and problem identifier ajaxGet = function (url) { return Q.Promise(function (resolve, reject, notify) { $.ajax({ @@ -407,6 +407,10 @@ ); languageCache[identifier] = x; return x; + }, + + 'getProblemDef': function (language, group, problem) { + return ajaxGet(ajaxPrefix + language + '/' + group + '/' + problem + '/problem.json'); } }; })(); diff --git a/js/codeq/hint.js b/js/codeq/hint.js index e1c7a6b..1b97afc 100644 --- a/js/codeq/hint.js +++ b/js/codeq/hint.js @@ -7,10 +7,11 @@ var firstCharacterPos = {'line': 0, 'ch': 0}, sel_no_scroll = {'scroll': false}; - codeq.makeHinter = function (jqHints, jqEditor, editor, hintDefs, planDef) { + codeq.makeHinter = function (jqHints, jqEditor, editor, trNamespace, hintDefsA, commonHintDefsA, planDef) { var hintCounter = 0, // for generating unique class-names hintCleaners = [], planIdx = 0, + dictionary = [], clearHints = function () { var i; @@ -130,6 +131,25 @@ } }; + codeq.tr.registerDictionary(trNamespace, dictionary); + + // TODO: below is a temporary code to bridge the old implementation with the new data format + if (planDef.sl) planDef = planDef.sl; + else planDef = planDef.en || []; + var hintDefs = {}, t1, t2, k; + if (hintDefsA.sl) t1 = hintDefsA.sl; + else t1 = hintDefsA.en || {}; + if (commonHintDefsA.sl) t2 = commonHintDefsA.sl; + else t2 = commonHintDefsA.en || {}; + for (k in t2) { + if (!t2.hasOwnProperty(k)) continue; + hintDefs[k] = t2[k]; + } + for (k in t1) { + if (!t1.hasOwnProperty(k)) continue; + hintDefs[k] = t1[k]; + } + return { /** Display the next "planning" hint and return whether there are * any more available. @@ -195,6 +215,7 @@ 'destroy': function () { clearHints(); + codeq.tr.unregisterDictionary(trNamespace); jqHints.empty(); jqHints = null; jqEditor = null; diff --git a/js/codeq/navigation.js b/js/codeq/navigation.js index 4445a3d..30503e0 100644 --- a/js/codeq/navigation.js +++ b/js/codeq/navigation.js @@ -57,7 +57,7 @@ History.pushState({'state': name, 'params': Array.prototype.slice.apply(arguments, [1])}, null, '?s=' + name); } catch (e) { - codeq.log.error('init: History.pushState() failed for new state ' + name+'. Error:'+e); + codeq.log.error('init: History.pushState() failed for new state ' + name+'. Error:'+e, e); } }, 'destroy': function () { diff --git a/js/codeq/problem.js b/js/codeq/problem.js index 3e218d8..35b45af 100644 --- a/js/codeq/problem.js +++ b/js/codeq/problem.js @@ -2,8 +2,93 @@ var jqScreen = $('#screen_problem'), languageCache = {}, // keyed by language identifier: processed data about languages translationCache = [], // keys are autogenerated in ta(), a value is a dictionary of translations of a translation key for every language + problemCache = {}, // problem data cache, 3-level, keyed by: language, problem group, and problem identifier langs, Nlangs, // constants, set on init + // ================================================================================ + // Hint processing: extract hints from the translations and return them in the + // processed form, hint key -> translation language -> value + // ================================================================================ + + processHints = function (rawTranslations) { + var defaultHint = {}, // here we put all the hints with their default translations + allHints = {}, // the result + allHintKeys = [], + tr, key, i, lang, hint, h, j; + // find the default hint translations, they will form the basis of default hints + tr = chooseDefaultTranslation(rawTranslations, 'hint') || {}; + for (key in tr) { // copy the hints + if (!tr.hasOwnProperty(key)) continue; + defaultHint[key] = tr[key]; + allHintKeys.push(key); + } + // copy any hints not in the default hints to the default hints + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + if (!tr || !tr.hint) continue; // skip unavailable translations or translations with no hints + hint = tr.hint; + for (key in hint) { + if (!hint.hasOwnProperty(key) || !hint[key]) continue; + if (!key in defaultHint) { + defaultHint[key] = hint[key]; + allHintKeys.push(key); + } + } + } + // create all translations for hints + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + // set up hints + if (!tr || !tr.hint) { + // there's no hint in the current language, copy the default in its entirety + allHints[lang] = defaultHint; + } + else { + // make a copy of all hints, using the default hint value where a hint value is missing + hint = {}; + allHints[lang] = hint; + h = tr.hint; + for (j = allHintKeys.length; j >= 0; j--) { + key = allHintKeys[j]; + hint[key] = h[key] || defaultHint[key]; + } + } + } + return allHints; + }, + + // ================================================================================ + // Plan processing: extract plans from the translations and return them in the + // processed form, hint key -> translation language -> value + // ================================================================================ + + processPlans = function (rawTranslations) { + // find the default plan translation + var defaultPlan = chooseDefaultTranslation(rawTranslations, 'plan') || [], + allPlans = {}, // the result + i, lang, tr; + // create all translations for plan + for (i = langs.length - 1; i >= 0; i--) { + lang = langs[i]; + tr = rawTranslations[lang]; + // set up plan + if (!tr || !tr.plan) { + // there's no plan in the current language, copy the default plan + allPlans[lang] = defaultPlan; + } + else { + allPlans[lang] = tr.plan; + } + } + return allPlans; + }, + + // ================================================================================ + // Group + problems directory processing + // ================================================================================ + chooseTranslation = function (keyword, lang, currentDict, enDict, translations) { var tr = currentDict[keyword], otherLang; @@ -65,11 +150,12 @@ */ createLanguageData = function (data, languageIdentifier) { // data is the content of language.json var li = languageIdentifier, // a shorthand + rawTranslations = data.translations || {}, groups = data.groups || {}, html = [], problemReferences = [], groupIdentifier, group, problems, problemIdentifier, problem; - var langDict = convertTranslations(data.translations, 'name', 'description'), // this will be the resulting dictionary: multi-level keys that lead up to the lang-dict + var langDict = convertTranslations(rawTranslations, 'name', 'description'), // this will be the resulting dictionary: multi-level keys that lead up to the lang-dict groupDict, problemDict; // title: HTML structure for "name" and "desc" html.push('


'); @@ -91,24 +177,29 @@ problem = problems[problemIdentifier] || {}; problemDict = convertTranslations(problem.translations, 'name'); html.push('
  • '); - problemReferences.push({'g': groupIdentifier, 'p': problemIdentifier}); + problemReferences.push({'g': groupIdentifier, 'p': problemIdentifier, 'id': problem.id}); } html.push(''); } html.push(''); return { - 'language': languageIdentifier, - 'html': html.join(''), - 'refs': problemReferences, - 'hints': {} // TODO: prepare common hints for the language + 'language': languageIdentifier, // 'prolog', 'python', ... + 'html': html.join(''), // the DOM structure (without textual content), as HTML text + 'refs': problemReferences, // array of problem info {g: group, p: problem, id: problem_id}, referenced from DOM elements + 'hints': processHints(rawTranslations) // hint translations: keyword -> lang -> value }; }, + // ================================================================================ + // DOM instantiation. + // The transition to problem solving takes place here, after the user clicks on a + // problem and all the required data is loaded. + // ================================================================================ /** * Instantiates the screen from the given processed data. */ - createDom = function (data) { + createDom = function (data) { // data is the (cached) result of createLanguageData() var language = data.language; jqScreen.html(data.html); codeq.tr.translateDom(jqScreen); @@ -119,12 +210,15 @@ codeq.log.error('Clicked on a problem link having erroneous index: ' + index); return; } - // transition codeq.wait( - codeq.comms.getProblem(language, ref.g, ref.p) - .then(function (data) { - if (data.code !== 0) throw new Error('Failed to obtain problem data, code: ' + data.code + ', message: ' + data.message); - codeq.globalStateMachine.transition(language, data); + Q.all([ + codeq.comms.getProblem(language, ref.g, ref.p), // TODO: use ref.id instead // the current solution + getProblemData(language, ref.g, ref.p) // the (cached) result of processProblemData() + ]) + .spread(function (userProblemData, generalProblemData) { + if (userProblemData.code !== 0) throw new Error('Failed to obtain user problem data, code: ' + userProblemData.code + ', message: ' + userProblemData.message); + if (!generalProblemData) throw new Error('General problem data is not defined'); + codeq.globalStateMachine.transition(language, generalProblemData, data.hints, userProblemData.solution); }) ) .fail(function (reason) { @@ -134,6 +228,61 @@ .done(); }); }, + + // ================================================================================ + // Problem definition processing + // ================================================================================ + + chooseDefaultTranslation = function (rawTranslations, translationKey) { + var tr = rawTranslations.en, // try English as the default + lang; + if (tr && tr[translationKey]) return tr[translationKey]; + for (lang in rawTranslations) { // find a translation with hints + if (!rawTranslations.hasOwnProperty(lang) || rawTranslations[lang]) continue; + tr = rawTranslations[lang]; + if (tr[translationKey]) return tr[translationKey]; + } + return null; // default must be chosen by the caller + }, + + processProblemData = function (rawData, language, group, problem) { + var rawTranslations = rawData.translations || {}; + return { + 'language': language, + 'group': group, + 'problem': problem, + 'id': rawData.id, + 'translations': convertTranslations(rawTranslations, 'title', 'name', 'slug', 'description'), // GUI translations: keyword -> lang -> value + 'hint': processHints(rawTranslations), // hint translations: keyword -> lang -> value + 'plan': processPlans(rawTranslations) // plan translations: keyword -> lang -> value + }; + }, + + getProblemData = function (language, group, problem) { + var langCache = problemCache[language], + groupCache, cachedProblem, promise; + if (langCache) { + groupCache = langCache[group]; + if (!groupCache) { + groupCache = {}; + langCache[group] = groupCache; + } + } + else { + langCache = {}; + problemCache[language] = langCache; + groupCache = {}; + langCache[group] = groupCache; + } + cachedProblem = groupCache[problem]; + if (cachedProblem) return Q(cachedProblem); + return codeq.comms.getProblemDef(language, group, problem).then(function (rawData) { + var data = processProblemData(rawData); + groupCache[problem] = data; + return data; + }); + }, + currentLanguage; // the currently active language // ================================================================================ diff --git a/js/codeq/prolog.js b/js/codeq/prolog.js index be6b0c8..cd3fcb4 100644 --- a/js/codeq/prolog.js +++ b/js/codeq/prolog.js @@ -66,7 +66,7 @@ }; var prologHandler; //created when we enter the prolog state and destroyed once we leave it codeq.globalStateMachine.register('prolog', { - 'enter': function (data) { + 'enter': function (problemDef, commonHints, currentSolution) { $('#navigation-login').css('display', ''); $('#navigation-language').css('display', ''); $('#navigation-problem').css('display', ''); @@ -74,7 +74,7 @@ $('#navigation-prolog').css('display', ''); jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly - prologHandler = createPrologHandler(data.data); + prologHandler = createPrologHandler(problemDef, commonHints, currentSolution); subScreens = codeq.makeStateMachine(substates); subScreens.transition(jqDescription.data(stateNameTag)); /* Q.delay(100).then(function(){ @@ -245,30 +245,36 @@ }; }; + codeq.on('init', function (args) { + codeq.tr.registerDictionary('prolog', codeq.tr.emptyDictionary); // to make the translator happy, when this screen is not active + }); + /** * Creates a new handler for the given Prolog assignment definition. * * @param {PrologTaskDef} info * @returns {{destroy: Function, processServerHints: Function}} */ - createPrologHandler = function (info) { - var problem = info.problem, + createPrologHandler = function (problemDef, commonHints, currentSolution) { + var //problem = info.problem, jqDescriptionContent = jqDescription.find('.description'), jqEditor = jqCode.find('.code_editor'), jqTerminal = jqConsole.find('.console'), jqHints = jqInfo.find('.hints'), editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true }), - activityHandler = makeActivityHandler(editor, problem.id), - terminal = makePrologTerminalHandler(jqTerminal, editor, problem.id, activityHandler), - hinter = codeq.makeHinter(jqHints, jqEditor, editor, problem.hint, problem.plan), + activityHandler = makeActivityHandler(editor, problemDef.id), + terminal = makePrologTerminalHandler(jqTerminal, editor, problemDef.id, activityHandler), + hinter = codeq.makeHinter(jqHints, jqEditor, editor, 'prolog_hints', problemDef.hint, commonHints, problemDef.plan), commError = function (error) { alert(error); }; - editor.setValue(info.solution); - $('#screen_prolog .title').text(problem.slug); - jqDescriptionContent.html(problem.description); - jqBtnPlan.prop('disabled', (problem.plan || '').length == 0); + codeq.tr.registerDictionary('prolog', problemDef.translations); + codeq.tr.translateDom(jqScreen); + if (currentSolution) editor.setValue(currentSolution); +// $('#screen_prolog .title').text(problem.slug); +// jqDescriptionContent.html(problem.description); + jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.en) || []).length == 0); editor.on('change', function (instance, changeObj) { var doc = editor.getDoc(), @@ -295,7 +301,7 @@ codeq.comms.sendHint({ 'language': 'prolog', 'program': editor.getDoc().getValue(), - 'problem_id': problem.id + 'problem_id': problemDef.id }) .then(function (data) { if (data.code === 0) { @@ -319,7 +325,7 @@ codeq.comms.sendTest({ 'language': 'prolog', 'program': editor.getDoc().getValue(), - 'problem_id': problem.id + 'problem_id': problemDef.id }) .then(function (data) { if (data.code === 0) { @@ -351,6 +357,7 @@ jqEditor = null; jqTerminal = null; jqHints = null; + codeq.tr.registerDictionary('prolog', codeq.tr.emptyDictionary); } }; }; diff --git a/js/codeq/python.js b/js/codeq/python.js index e80bbb7..8b66881 100644 --- a/js/codeq/python.js +++ b/js/codeq/python.js @@ -71,7 +71,7 @@ }; var pythonHandler; //created when we enter the python state and destroyed once we leave it codeq.globalStateMachine.register('python', { - 'enter': function (data) { + 'enter': function (problemDef, commonHints, currentSolution) { $('#navigation-login').css('display', ''); $('#navigation-language').css('display', ''); $('#navigation-problem').css('display', ''); @@ -79,7 +79,7 @@ $('#navigation-python').css('display', ''); jqScreen.css('display', '');//we have to show the screen now so the code editor shows its initial values correctly - pythonHandler = createPythonHandler(data.data); + pythonHandler = createPythonHandler(problemDef, commonHints, currentSolution); subScreens = codeq.makeStateMachine(substates); subScreens.transition(jqDescription.data(stateNameTag)); /* Q.delay(100).then(function(){ @@ -203,7 +203,9 @@ }; }; - + codeq.on('init', function (args) { + codeq.tr.registerDictionary('python', codeq.tr.emptyDictionary); // to make the translator happy, when this screen is not active + }); /** * Creates a new handler for the given Prolog assignment definition. @@ -211,24 +213,26 @@ * @param {PrologTaskDef} info * @returns {{destroy: Function, processServerHints: Function}} */ - var createPythonHandler = function (info) { - var problem = info.problem, + var createPythonHandler = function (problemDef, commonHints, currentSolution) { + var //problem = info.problem, jqDescriptionContent = jqDescription.find('.description'), jqEditor = jqCode.find('.code_editor'), jqTerminal = jqConsole.find('.console'), jqHints = jqInfo.find('.hints'), editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true, mode: 'python' }), - activityHandler = makeActivityHandler(editor, problem.id), - terminal = makePythonTerminalHandler(jqTerminal, editor, problem.id, activityHandler), - hinter = codeq.makeHinter(jqHints, jqEditor, editor, problem.hint, problem.plan), + activityHandler = makeActivityHandler(editor, problemDef.id), + terminal = makePythonTerminalHandler(jqTerminal, editor, problemDef.id, activityHandler), + hinter = codeq.makeHinter(jqHints, jqEditor, editor, 'python_hints', problemDef.hint, commonHints, problemDef.plan), commError = function (error) { alert(error); }; - editor.setValue(info.solution); - $('#screen_python .title').text(problem.slug); - jqDescriptionContent.html(problem.description); - jqBtnPlan.prop('disabled', (problem.plan || '').length == 0); + codeq.tr.registerDictionary('python', problemDef.translations); + codeq.tr.translateDom(jqScreen); + if (currentSolution) editor.setValue(currentSolution); +// $('#screen_python .title').text(problem.slug); +// jqDescriptionContent.html(problem.description); + jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.en) || []).length == 0); editor.on('change', function (instance, changeObj) { var doc = editor.getDoc(), @@ -253,7 +257,7 @@ codeq.comms.sendHint({ 'language': 'python', 'program': editor.getDoc().getValue(), - 'problem_id': problem.id + 'problem_id': problemDef.id }) .then(function (data) { if (data.code === 0) { @@ -271,7 +275,7 @@ codeq.comms.sendTest({ 'language': 'python', 'program': editor.getDoc().getValue(), - 'problem_id': problem.id + 'problem_id': problemDef.id }) .then(function (data) { if (data.code === 0) { @@ -323,6 +327,7 @@ jqEditor = null; jqTerminal = null; jqHints = null; + codeq.tr.registerDictionary('python', codeq.tr.emptyDictionary); } }; }; diff --git a/js/codeq/translation.js b/js/codeq/translation.js index af39cdc..d4278ac 100644 --- a/js/codeq/translation.js +++ b/js/codeq/translation.js @@ -6,6 +6,7 @@ translationKey = jqElt.data('tkey'), dict = dicts[dictionaryKey], translations, html, key; + if (dict === codeq.tr.emptyDictionary) return; // silent ignore if (!dict) { codeq.log.error('Cannot find translation dictionary ' + dictionaryKey); return; @@ -61,6 +62,12 @@ dicts[name] = dict; }, + 'unregisterDictionary': function (name) { + delete dicts[name]; + }, + + 'emptyDictionary': {}, // use this with registerDictionary when you don't want any translations + 'translateDom': function (jqTopElt) { var lang = codeq.getLang(); jqTopElt.find('.translatable').each(function () { -- cgit v1.2.1