summaryrefslogtreecommitdiff
path: root/js/codeq
diff options
context:
space:
mode:
authorAleš Smodiš <aless@guru.si>2015-09-30 16:32:07 +0200
committerAleš Smodiš <aless@guru.si>2015-09-30 16:32:07 +0200
commit20ba9213950544c6a7f1dd04af160bf674561e3b (patch)
tree7f33742a84906830a34f3bfe7f535b52bcf72068 /js/codeq
parent7c65ca7a98e681eee5d1910c10e57072caee1799 (diff)
Implemented structural translation of hints and plans.
Diffstat (limited to 'js/codeq')
-rw-r--r--js/codeq/hint.js238
-rw-r--r--js/codeq/problem.js38
-rw-r--r--js/codeq/python.js2
-rw-r--r--js/codeq/translation.js1
4 files changed, 225 insertions, 54 deletions
diff --git a/js/codeq/hint.js b/js/codeq/hint.js
index d30f934..6f54717 100644
--- a/js/codeq/hint.js
+++ b/js/codeq/hint.js
@@ -12,6 +12,7 @@
hintCleaners = [],
planIdx = 0,
dictionary = [],
+ jqHintsContainer = jqHints.parent(),
clearHints = function () {
var i;
@@ -44,45 +45,192 @@
});
},
- typeHandlers = {
- 'static': function (type, template, serverHint) {
- var args = serverHint ? serverHint.args : null,
- jqContainer, jqButton, i, N, tmpl, tmplIsObject;
- if (template instanceof Array) { // unwrap the template if there's only one
- if (template.length == 0) template = '';
- else if (template.length == 1) template = template[0] + ''; // it must be a string
+ prepareStaticHintContent = function (hintContent, indices, hintId) {
+ var content = {},
+ langs = codeq.availableLangs,
+ skippedLangs = [],
+ Nhints = 0, // count hints and remember the count
+ haveIndices = indices instanceof Array,
+ i, lang, j, node, tmpContent;
+ langLoop:
+ for (j = langs.length - 1; j >= 0; j--) {
+ lang = langs[j];
+ node = hintContent[lang];
+ if (!node) {
+ // no translation for this language, mark for later
+ skippedLangs.push(lang);
+ }
+ else {
+ if (haveIndices) {
+ // traverse node's indices
+ for (i = 0; i < indices.length; i++) {
+ node = node[indices[i]];
+ if (!node) {
+ // index out of bounds, mark this language for later
+ codeq.log.error('Cannot reference hint ' + hintId + ' with indices ' + indices);
+ skippedLangs.push(lang);
+ continue langLoop;
+ }
+ }
+ }
+ if (typeof node === 'string') {
+ // we have a single string (= a simplex message), put it into an array
+ content[lang] = [node];
+ if (Nhints < 1) Nhints = 1;
+ }
+ else if (node instanceof Array) {
+ // we already have an array
+ if (node.length == 0) {
+ if (haveIndices) codeq.log.error('Hint ' + hintId + ' with indices ' + indices + ' for language ' + lang + ' contains an empty array');
+ else codeq.log.error('Hint ' + hintId + ' for language ' + lang + ' contains an empty array');
+ skippedLangs.push(lang);
+ continue;
+ }
+ // verify that each array element is a string or an object with a message
+ for (i = node.length - 1; i >= 0; i--) {
+ tmpContent = node[i];
+ if (typeof tmpContent === 'string') {
+ // this is okay
+ }
+ else if (tmpContent && (typeof tmpContent === 'object')) {
+ if (typeof tmpContent.message !== 'string') {
+ if (haveIndices) tmpContent.message = 'There is no message defined for hint ' + hintId + 'with indices ' + indices + ' in language ' + lang + ' at index ' + i;
+ else tmpContent.message = 'There is no message defined for hint ' + hintId + ' in language ' + lang + ' at index ' + i;
+ codeq.log.error(tmpContent.message);
+ }
+ }
+ else {
+ // not a string or an object with a message
+ if (haveIndices) tmpContent = 'There is no message defined for hint ' + hintId + 'with indices ' + indices + ' in language ' + lang + ' at index ' + i;
+ else tmpContent = 'There is no message defined for hint ' + hintId + ' in language ' + lang + ' at index ' + i;
+ node[i] = tmpContent;
+ codeq.log.error(tmpContent);
+ }
+ }
+ content[lang] = node;
+ if (Nhints < node.length) Nhints = node.length;
+ }
+ else if (node && (typeof node === 'object') && (typeof node.message === 'string')) {
+ // we have a single object with a message (= a complex message), put it into an array
+ content[lang] = [node];
+ if (Nhints < 1) Nhints = 1;
+ }
+ else {
+ if (haveIndices) codeq.log.error('Hint ' + hintId + ' with indices ' + indices + ' did not result in a terminal node for language ' + lang + ', but: ' + node);
+ else codeq.log.error('Hint ' + hintId + ' probably needs indices because it does not have a terminal node for language ' + lang + ', but: ' + node);
+ skippedLangs.push(lang);
+ }
}
- if (template instanceof Array) {
- codeq.log.debug('Processing an array of static hints');
+ }
+ if (Nhints === 0) {
+ // provide error feedback on display when there is no hint translation available in any language
+ if (haveIndices) tmpContent = ['No hints found for hint ' + hintId + ' with indices ' + indices];
+ else tmpContent = ['No hints found for hint ' + hintId];
+ codeq.log.error(tmpContent[0]);
+ for (j = langs.length - 1; j >= 0; j--) {
+ content[langs[j]] = tmpContent;
+ }
+ Nhints = 1;
+ }
+ else if (skippedLangs.length > 0) {
+ // choose a default content and assign it to skipped languages
+ lang = 'en'; // try English first
+ tmpContent = content[lang];
+ if (!tmpContent) {
+ // if no English exists, find one that does
+ for (lang in content) {
+ if (!content.hasOwnProperty(lang)) continue;
+ tmpContent = content[lang];
+ if (tmpContent) break;
+ }
+ }
+ codeq.log.error('Translations in languages ' + skippedLangs.join(', ') + ' are missing or erroneous for hint ' + hintId + ', replacing their content with translation for ' + lang);
+ // assign the default content to skipped languages
+ for (j = skippedLangs.length - 1; j >= 0; j--) {
+ content[skippedLangs[j]] = tmpContent;
+ }
+ }
+ content.hintLength = Nhints;
+ return content;
+ },
+
+ ta = function (trObj) { // an object of the form: {'en': 'english content', 'sl': 'slovenska vsebina'}
+ var result = ['data-dict="', trNamespace, '" data-tkey="', dictionary.length, '"'].join('');
+ dictionary.push(trObj);
+ return result;
+ },
+
+ typeHandlers = {
+ 'static': function (type, template, serverHint, hintId) {
+ var content = prepareStaticHintContent(template, serverHint.indices, hintId),
+ args = serverHint ? serverHint.args : null,
+ hintIndex = 0,
+ trButton = {},
+ Nhints = content.hintLength,
+ nextJqHint = function () {
+ var trContent = {},
+ langs = codeq.availableLangs,
+ lang, i, msg, jq, deltaHeight;
+ for (i = langs.length - 1; i >= 0; i--) {
+ lang = langs[i];
+ try {
+ msg = content[lang][hintIndex];
+ if (typeof msg === 'string') {
+ trContent[lang] = processTemplate(msg, args);
+ trButton[lang] = 'More...';
+ }
+ else {
+ trContent[lang] = processTemplate(msg.message, args);
+ trButton[lang] = msg.linkText;
+ }
+ }
+ catch (e) {
+ msg = 'Error processing hint ' + hintId + ' at index ' + hintIndex + ' for language ' + lang + ': ' + e;
+ codeq.log.error(msg, e);
+ trContent[lang] = msg;
+ }
+ }
+ jq = $('<div class="hint-static translatable" ' + ta(trContent) + '></div>');
+ hintIndex++;
+ if (jqButton) {
+ if (hintIndex < Nhints) {
+ jqButton.before(jq);
+ codeq.tr.translateDom(jqButton);
+ }
+ else {
+ jqButton.remove();
+ jqContainer.append(jq);
+ }
+ }
+ else {
+ jqContainer.append(jq);
+ }
+ codeq.tr.translateDom(jq);
+ // scroll into view if overflowing
+ deltaHeight = jqHints.height() - jqHintsContainer.height();
+ if (deltaHeight > 0) {
+ jqHintsContainer.scrollTop(deltaHeight);
+ }
+ },
+ jqContainer, jqButton;
+
+ if (Nhints > 1) {
+ // hint sequence
jqContainer = $('<div class="hint-static-group"></div>');
- jqButton = $('<a class="hint-static-link"></a>');
+ jqButton = $('<a class="hint-static-link translatable" ' + ta(trButton) + '></a>');
jqHints.append(jqContainer);
- N = template.length;
- tmpl = template[0];
- tmplIsObject = (typeof tmpl === 'object') && (tmpl !== null);
- jqContainer.append('<div class="hint-static">' + processTemplate((tmplIsObject ? tmpl.message : tmpl) || '', args) + '</div>');
jqContainer.append(jqButton);
- jqButton.text(tmplIsObject && tmpl.linkText ? tmpl.linkText : 'More...'); // TODO: translate "more"
- i = 1;
jqButton.on('click', function () {
- var tmpl = template[i],
- tmplIsObject = (typeof tmpl === 'object') && (tmpl !== null),
- jqNext = $('<div class="hint-static">' + processTemplate((tmplIsObject ? tmpl.message : tmpl) || '', args) + '</div>');
- i++;
- if (i < N) {
- jqButton.before(jqNext);
- jqButton.text(tmplIsObject && tmpl.linkText ? tmpl.linkText : 'More...'); // TODO: translate "more"
- }
- else {
- jqButton.remove();
- jqContainer.append(jqNext);
- }
+ nextJqHint();
});
}
else {
- codeq.log.debug('Processing a single static hint');
- jqHints.append('<div class="hint-static">' + processTemplate(template, args) + '</div>');
+ // a single hint
+ jqContainer = jqHints;
+ jqButton = null;
}
+ nextJqHint();
+
// no hint cleaner here, a static hint remains on the screen
},
@@ -137,7 +285,7 @@
hintCommonDefs = commonDef.hint_type,
hintProblemTr = problemDef.hint,
hintCommonTr = commonDef.hint,
- planDef = problemDef.plan.sl;
+ planDef = problemDef.plan;
return {
/**
@@ -146,12 +294,16 @@
*/
'planNext': function () {
if (planIdx < planDef.length) {
- typeHandlers['static']('static', planDef[planIdx], null);
+ typeHandlers.static('static', planDef[planIdx], {}, 'plan');
planIdx++;
}
return planIdx < planDef.length;
},
+ 'hasNextPlan': function () {
+ return planIdx < planDef.length;
+ },
+
/**
* Processes and display appropriately the server hints.
* TODO: sort hints so static and popup hints come first, and a (single) drop-down hint last
@@ -160,7 +312,8 @@
*/
'handle': function (serverHints) {
var n = serverHints.length,
- i, serverHint, hintId, hintDef, hintContent, hintType, hintTemplate, t, fn, indices;
+ i, serverHint, hintId, hintDef, hintContent, hintType, t, fn, indices;
+
clearHints();
mainLoop:
for (i = 0; i < n; i++) {
@@ -182,28 +335,18 @@
codeq.log.error('Hint without content: ' + hintId);
continue;
}
- if (serverHint.indices) {
- indices = serverHint.indices;
- for (i = 0; i < indices.length; i++) {
- hintContent = hintContent[indices[i]];
- if (!hintContent) {
- codeq.log.error('Cannot reference hint ' + hintId + ' with indices ' + serverHint.indices);
- continue mainLoop;
- }
- }
- }
t = typeof hintDef;
- if (t === 'string') hintType = hintDef;
- else if ((t === 'object') && (hintDef !== null)) hintType = hintDef.type;
+ if (t === 'string') hintType = hintDef; // currently a hint type is a string
+ else if ((t === 'object') && (hintDef !== null)) hintType = hintDef.type; // but in future we may use an object, if a definition becomes more complex
else {
- codeq.log.error('Cannot determine type of hint ' + hintId + ' from: ' + hintDef);
+ codeq.log.error('Cannot determine the type of hint ' + hintId + ' from: ' + hintDef);
continue;
}
fn = typeHandlers[hintType];
if (!fn) codeq.log.error('Unsupported hint type: ' + hintType);
- else fn(hintType, hintContent.sl, serverHint);
+ else fn(hintType, hintContent, serverHint, hintId);
}
},
@@ -213,6 +356,7 @@
jqHints.empty();
jqHints = null;
jqEditor = null;
+ jqHintsContainer = null;
editor = null;
}
};
diff --git a/js/codeq/problem.js b/js/codeq/problem.js
index d07e5fa..f6331b3 100644
--- a/js/codeq/problem.js
+++ b/js/codeq/problem.js
@@ -83,25 +83,51 @@
if (!translation || !(translation instanceof Array)) return false;
return translation.length > 0;
},
+ /**
+ * Structurally converts the plan definition into something that the
+ * hint module can work with.
+ * The input is the translations object from problem.json in the form
+ * {'sl': { ..., 'plan': [hint1_sl, hint2_sl, ...]}, 'en': { ..., 'plan': [hint1_en, hint2_en, ...]}, ...}
+ * The output is a list of hints in the plan, translations are in each hint:
+ * [{'sl': hint1_sl, 'en': hint1_en, ...}, {'sl': hint2_sl, 'en': hint2_en}, ...]
+ */
processPlans = function (rawTranslations) {
// find the default plan translation
var defaultPlan = chooseDefaultTranslation(rawTranslations, 'plan', defaultPlanCondition) || [],
- allPlans = {}, // the result
- i, lang, tr;
- // create all translations for plan
+ result = [],
+ i, lang, tr, plan, j, fragment;
+ if (defaultPlan.length == 0) return result; // no plan
+ // copy all translations from plan
for (i = langs.length - 1; i >= 0; i--) {
lang = langs[i];
tr = rawTranslations[lang];
// set up plan
if (tr && defaultPlanCondition(tr.plan)) {
- allPlans[lang] = tr.plan;
+ plan = tr.plan;
}
else {
// there's no plan in the current language, copy the default plan
- allPlans[lang] = defaultPlan;
+ plan = defaultPlan;
+ }
+ if (!(plan instanceof Array)) plan = [plan];
+ for (j = 0; j < plan.length; j++) {
+ if (j < result.length) fragment = result[j];
+ else {
+ fragment = {};
+ result.push(fragment);
+ }
+ fragment[lang] = plan[j];
}
}
- return allPlans;
+ // ensure each plan element has all translations
+ for (j = result.length - 1; j >= 0; j--) {
+ fragment = result[j];
+ for (i = langs.length - 1; i >= 0; i--) {
+ lang = langs[i];
+ if (!fragment[lang]) fragment[lang] = 'Missing plan for language ' + lang + ' at index ' + j;
+ }
+ }
+ return result;
},
// ================================================================================
diff --git a/js/codeq/python.js b/js/codeq/python.js
index 8a9e92f..59e010a 100644
--- a/js/codeq/python.js
+++ b/js/codeq/python.js
@@ -233,7 +233,7 @@
if (currentSolution) editor.setValue(currentSolution);
// $('#screen_python .title').text(problem.slug);
// jqDescriptionContent.html(problem.description);
- jqBtnPlan.prop('disabled', ((problemDef.plan && problemDef.plan.sl) || []).length == 0);
+ jqBtnPlan.prop('disabled', !hinter.hasNextPlan());
editor.on('change', function (instance, changeObj) {
var doc = editor.getDoc(),
diff --git a/js/codeq/translation.js b/js/codeq/translation.js
index d4278ac..f33199d 100644
--- a/js/codeq/translation.js
+++ b/js/codeq/translation.js
@@ -70,6 +70,7 @@
'translateDom': function (jqTopElt) {
var lang = codeq.getLang();
+ if (jqTopElt.hasClass('translatable')) translateElement(jqTopElt, lang);
jqTopElt.find('.translatable').each(function () {
translateElement($(this), lang);
});