diff --git a/curriculum/getChallenges.js b/curriculum/getChallenges.js index 22ec94492f..0b68376294 100644 --- a/curriculum/getChallenges.js +++ b/curriculum/getChallenges.js @@ -3,6 +3,13 @@ const { findIndex } = require('lodash'); const readDirP = require('readdirp-walk'); const { parseMarkdown } = require('../tools/challenge-md-parser'); 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'); @@ -83,6 +90,22 @@ async function buildCurriculum(file, curriculum) { 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) { let meta; if (maybeMeta) { @@ -95,7 +118,16 @@ async function createChallenge(fullPath, maybeMeta) { meta = require(metaPath); } 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( meta.challengeOrder, ([id]) => id === challenge.id @@ -131,6 +163,41 @@ async function createChallenge(fullPath, maybeMeta) { 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) { const [maybeSuper] = filePath.split(path.sep); return superBlockInfo(maybeSuper); diff --git a/tools/challenge-md-parser/translation-parser/translation-parser.js b/tools/challenge-md-parser/translation-parser/translation-parser.js new file mode 100644 index 0000000000..44049dbae5 --- /dev/null +++ b/tools/challenge-md-parser/translation-parser/translation-parser.js @@ -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, + '(^[^\'"`]*?(?.*?<\/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, '()'); +} + +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; +} diff --git a/tools/challenge-md-parser/translation-parser/translation-parser.test.js b/tools/challenge-md-parser/translation-parser/translation-parser.test.js index 93459a2122..323618ed31 100644 --- a/tools/challenge-md-parser/translation-parser/translation-parser.test.js +++ b/tools/challenge-md-parser/translation-parser/translation-parser.test.js @@ -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', () => { const seed = `inline comment // Add your code below this line Add your code above this line `;