feat: parse translated challenges
Using the English challenge as a source for the seed, solution and tests this takes the parts that can be translated from the translated version of the challenge. It also translates known comments in the seed.
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
d41e44ebf9
commit
0952ca6bfd
@ -3,6 +3,13 @@ const { findIndex } = require('lodash');
|
|||||||
const readDirP = require('readdirp-walk');
|
const readDirP = require('readdirp-walk');
|
||||||
const { parseMarkdown } = require('../tools/challenge-md-parser');
|
const { parseMarkdown } = require('../tools/challenge-md-parser');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
const {
|
||||||
|
mergeChallenges,
|
||||||
|
translateCommentsInChallenge
|
||||||
|
} = require('../tools/challenge-md-parser/translation-parser/translation-parser');
|
||||||
|
/* eslint-enable max-len*/
|
||||||
|
const { COMMENT_TRANSLATIONS } = require('./comment-dictionary');
|
||||||
|
|
||||||
const { dasherize } = require('../utils/slugs');
|
const { dasherize } = require('../utils/slugs');
|
||||||
|
|
||||||
@ -83,6 +90,22 @@ async function buildCurriculum(file, curriculum) {
|
|||||||
challengeBlock.challenges = [...challengeBlock.challenges, challenge];
|
challengeBlock.challenges = [...challengeBlock.challenges, challenge];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function parseTranslation(engPath, transPath, dict) {
|
||||||
|
const engChal = await parseMarkdown(engPath);
|
||||||
|
const translatedChal = await parseMarkdown(transPath);
|
||||||
|
const codeLang = engChal.files[0] ? engChal.files[0].ext : null;
|
||||||
|
|
||||||
|
const engWithTranslatedComments = translateCommentsInChallenge(
|
||||||
|
engChal,
|
||||||
|
getChallengeLang(transPath),
|
||||||
|
dict,
|
||||||
|
codeLang
|
||||||
|
);
|
||||||
|
return mergeChallenges(engWithTranslatedComments, translatedChal);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.parseTranslation = parseTranslation;
|
||||||
|
|
||||||
async function createChallenge(fullPath, maybeMeta) {
|
async function createChallenge(fullPath, maybeMeta) {
|
||||||
let meta;
|
let meta;
|
||||||
if (maybeMeta) {
|
if (maybeMeta) {
|
||||||
@ -95,7 +118,16 @@ async function createChallenge(fullPath, maybeMeta) {
|
|||||||
meta = require(metaPath);
|
meta = require(metaPath);
|
||||||
}
|
}
|
||||||
const { name: superBlock } = superBlockInfoFromFullPath(fullPath);
|
const { name: superBlock } = superBlockInfoFromFullPath(fullPath);
|
||||||
const challenge = await parseMarkdown(fullPath);
|
if (!isAcceptedLanguage(getChallengeLang(fullPath)))
|
||||||
|
throw Error(`${getChallengeLang(fullPath)} is not a accepted language.
|
||||||
|
Trying to parse ${fullPath}`);
|
||||||
|
const challenge = await (isEnglishChallenge(fullPath)
|
||||||
|
? parseMarkdown(fullPath)
|
||||||
|
: parseTranslation(
|
||||||
|
getEnglishPath(fullPath),
|
||||||
|
fullPath,
|
||||||
|
COMMENT_TRANSLATIONS
|
||||||
|
));
|
||||||
const challengeOrder = findIndex(
|
const challengeOrder = findIndex(
|
||||||
meta.challengeOrder,
|
meta.challengeOrder,
|
||||||
([id]) => id === challenge.id
|
([id]) => id === challenge.id
|
||||||
@ -131,6 +163,41 @@ async function createChallenge(fullPath, maybeMeta) {
|
|||||||
|
|
||||||
exports.createChallenge = createChallenge;
|
exports.createChallenge = createChallenge;
|
||||||
|
|
||||||
|
function getEnglishPath(fullPath) {
|
||||||
|
const posix = path.posix.normalize(fullPath);
|
||||||
|
const match = posix.match(/(.*curriculum\/challenges\/)([^/]*)(.*)(\2)(.*)/);
|
||||||
|
const lang = getChallengeLang(fullPath);
|
||||||
|
if (!isAcceptedLanguage(lang))
|
||||||
|
throw Error(`${getChallengeLang(fullPath)} is not a accepted language.
|
||||||
|
Trying to parse ${fullPath}`);
|
||||||
|
if (match) {
|
||||||
|
return path.join(match[1], 'english', match[3] + 'english' + match[5]);
|
||||||
|
} else {
|
||||||
|
throw Error(`Malformed challenge path, ${fullPath} unable to parse.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChallengeLang(fullPath) {
|
||||||
|
const match = fullPath.match(/\.(\w+)\.md$/);
|
||||||
|
if (!match || match.length < 2)
|
||||||
|
throw Error(`Missing language extension for
|
||||||
|
${fullPath}`);
|
||||||
|
return fullPath.match(/\.(\w+)\.md$/)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnglishChallenge(fullPath) {
|
||||||
|
return getChallengeLang(fullPath) === 'english';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAcceptedLanguage(lang) {
|
||||||
|
const acceptedLanguages = ['english', 'chinese'];
|
||||||
|
return acceptedLanguages.includes(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getChallengeLang = getChallengeLang;
|
||||||
|
exports.getEnglishPath = getEnglishPath;
|
||||||
|
exports.isEnglishChallenge = isEnglishChallenge;
|
||||||
|
|
||||||
function superBlockInfoFromPath(filePath) {
|
function superBlockInfoFromPath(filePath) {
|
||||||
const [maybeSuper] = filePath.split(path.sep);
|
const [maybeSuper] = filePath.split(path.sep);
|
||||||
return superBlockInfo(maybeSuper);
|
return superBlockInfo(maybeSuper);
|
||||||
|
@ -0,0 +1,102 @@
|
|||||||
|
const clone = require('lodash/cloneDeep');
|
||||||
|
|
||||||
|
exports.translateComments = (text, lang, dict, codeLang) => {
|
||||||
|
const knownComments = Object.keys(dict);
|
||||||
|
const config = { knownComments, dict, lang };
|
||||||
|
switch (codeLang) {
|
||||||
|
case 'js':
|
||||||
|
return transMultiline(transInline(text, config), config);
|
||||||
|
case 'jsx':
|
||||||
|
return transJSX(text, config);
|
||||||
|
case 'html':
|
||||||
|
return transHTML(transCSS(text, config), config);
|
||||||
|
default:
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.translateCommentsInChallenge = (challenge, lang, dict, codeLang) => {
|
||||||
|
const challClone = clone(challenge);
|
||||||
|
|
||||||
|
if (challClone.files[0] && challClone.files[0].contents) {
|
||||||
|
challClone.files[0].contents = this.translateComments(
|
||||||
|
challenge.files[0].contents,
|
||||||
|
lang,
|
||||||
|
dict,
|
||||||
|
codeLang
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return challClone;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.mergeChallenges = (engChal, transChal) => {
|
||||||
|
if (!transChal.tests || transChal.tests.length !== engChal.tests.length)
|
||||||
|
throw Error(
|
||||||
|
`Challenges in both languages must have the same number of tests.
|
||||||
|
title: ${engChal.title}
|
||||||
|
localeTitle: ${transChal.localeTitle}`
|
||||||
|
);
|
||||||
|
const translatedTests = transChal.tests.map(({ text }, id) => ({
|
||||||
|
text,
|
||||||
|
testString: engChal.tests[id].testString
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
...engChal,
|
||||||
|
description: transChal.description,
|
||||||
|
instructions: transChal.instructions,
|
||||||
|
localeTitle: transChal.localeTitle,
|
||||||
|
forumTopicId: transChal.forumTopicId,
|
||||||
|
tests: translatedTests
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// bare urls could be interpreted as comments, so we have to lookbehind for
|
||||||
|
// http:// or https://
|
||||||
|
function transInline(text, config) {
|
||||||
|
return translateGeneric(
|
||||||
|
text,
|
||||||
|
config,
|
||||||
|
'(^[^\'"`]*?(?<!https?:)//\\s*)',
|
||||||
|
'(\\s*$)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transMultiline(text, config) {
|
||||||
|
return translateGeneric(text, config, '(/\\*\\s*)', '(\\s*\\*/)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS has to be handled separately since it is looking for comments inside tags
|
||||||
|
function transCSS(text, config) {
|
||||||
|
const regex = /<style>.*?<\/style>/gms;
|
||||||
|
const matches = text.matchAll(regex);
|
||||||
|
|
||||||
|
for (const [match] of matches) {
|
||||||
|
text = text.replace(match, transMultiline(match, config));
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transJSX(text, config) {
|
||||||
|
return translateGeneric(text, config, '({[^}]*/\\*\\s*)', '(\\s*\\*/[^{]*})');
|
||||||
|
}
|
||||||
|
|
||||||
|
function transHTML(text, config) {
|
||||||
|
return translateGeneric(text, config, '(<!--\\s*)', '(\\s*-->)');
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateGeneric(text, config, regexBefore, regexAfter) {
|
||||||
|
const { knownComments, dict, lang } = config;
|
||||||
|
const regex = new RegExp(regexBefore + '(.*?)' + regexAfter, 'gms');
|
||||||
|
const matches = text.matchAll(regex);
|
||||||
|
|
||||||
|
for (const [match, before, comment, after] of matches) {
|
||||||
|
if (knownComments.includes(comment)) {
|
||||||
|
text = text.replace(match, `${before}${dict[comment][lang]}${after}`);
|
||||||
|
} else {
|
||||||
|
console.warn(`${comment} does not appear in the comment dictionary`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
@ -154,6 +154,19 @@ describe('translation parser', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not translate urls', () => {
|
||||||
|
const seed = `http:// Add your code below this line
|
||||||
|
Add your code above this line `;
|
||||||
|
expect(translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js')).toBe(
|
||||||
|
seed
|
||||||
|
);
|
||||||
|
const seedS = `https:// Add your code below this line
|
||||||
|
Add your code above this line `;
|
||||||
|
expect(
|
||||||
|
translateComments(seedS, 'chinese', SIMPLE_TRANSLATION, 'js')
|
||||||
|
).toBe(seedS);
|
||||||
|
});
|
||||||
|
|
||||||
it('replaces inline English comments with their translations', () => {
|
it('replaces inline English comments with their translations', () => {
|
||||||
const seed = `inline comment // Add your code below this line
|
const seed = `inline comment // Add your code below this line
|
||||||
Add your code above this line `;
|
Add your code above this line `;
|
||||||
|
Reference in New Issue
Block a user