diff options
author | Aleš Smodiš <aless@guru.si> | 2015-09-28 18:44:48 +0200 |
---|---|---|
committer | Aleš Smodiš <aless@guru.si> | 2015-09-28 18:44:48 +0200 |
commit | 7dab4d3640b7b37c407eea111eda1fc0b71adbda (patch) | |
tree | f6d422f294d796706634de43826bce117fef409a | |
parent | 1e0c68482267f7e7393d2cde5042fcd863b8e23b (diff) |
Implement in-structure GUI translation for python and problem editing screens.
Hints are not yet covered.
-rw-r--r-- | index.html | 10 | ||||
-rw-r--r-- | js/codeq/comms.js | 8 | ||||
-rw-r--r-- | js/codeq/hint.js | 23 | ||||
-rw-r--r-- | js/codeq/navigation.js | 2 | ||||
-rw-r--r-- | js/codeq/problem.js | 173 | ||||
-rw-r--r-- | js/codeq/prolog.js | 33 | ||||
-rw-r--r-- | js/codeq/python.js | 33 | ||||
-rw-r--r-- | js/codeq/translation.js | 7 |
8 files changed, 241 insertions, 48 deletions
@@ -92,7 +92,7 @@ <h2>Python</h2> <p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cras mattis consectetur purus sit amet fermentum. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.</p> </div><!-- /.col-lg-4 --> - <div class="col-lg-4" id="choose-python"> + <div class="col-lg-4" id="choose-robot"> <img class="img-circle" src=res/eve.png alt="Generic placeholder image" width="140" height="140"> <h2>Robot</h2> <p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cras mattis consectetur purus sit amet fermentum. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.</p> @@ -107,8 +107,8 @@ <div class="container-fluid quadrants block1" id="screen_prolog" style="display: none;"> <div class="row"> <div class="col-lg-3 col-md-6 col-sm-12 block block1"> - <h2 class="title"></h2> - <div class="description"></div> + <h2 class="title translatable" data-dict="prolog" data-tkey="slug"></h2> + <div class="description translatable" data-dict="prolog" data-tkey="description"></div> <div class="block-label">Instructions</div> </div> <div class="col-lg-3 col-md-6 col-sm-12 block block2"> @@ -137,8 +137,8 @@ <div class="container-fluid quadrants block1" id="screen_python" style="display: none;"> <div class="row"> <div class="col-lg-3 col-md-6 col-sm-12 block block1"> - <h2 class="title"></h2> - <div class="description"></div> + <h2 class="title translatable" data-dict="python" data-tkey="slug"></h2> + <div class="description translatable" data-dict="python" data-tkey="description"></div> <div class="block-label">Instructions</div> </div> <div class="col-lg-3 col-md-6 col-sm-12 block block2"> 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('<h1 class="language-title translatable" ', ta(langDict.name), '></h1><hr>'); @@ -91,24 +177,29 @@ problem = problems[problemIdentifier] || {}; problemDict = convertTranslations(problem.translations, 'name'); html.push('<li><a class="problem-', '' + problemReferences.length, ' translatable" ', ta(problemDict.name), '></a></li>'); - problemReferences.push({'g': groupIdentifier, 'p': problemIdentifier}); + problemReferences.push({'g': groupIdentifier, 'p': problemIdentifier, 'id': problem.id}); } html.push('</ul></li>'); } html.push('</ul>'); 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 <a> 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 () { |