chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
const ENGLISH_CHALLENGE_NO_FILES = {
|
||||
id: 'id',
|
||||
title: 'Title',
|
||||
challengeType: 0,
|
||||
videoUrl: 'https://scrimba.com/',
|
||||
forumTopicId: 12345,
|
||||
tests: [
|
||||
{
|
||||
text: 'Test text',
|
||||
testString: 'assertions'
|
||||
},
|
||||
{
|
||||
text: 'Test text2',
|
||||
testString: 'assertions2'
|
||||
}
|
||||
],
|
||||
solutions: ['solution html string'],
|
||||
description: 'description html string',
|
||||
instructions: 'instructions html string',
|
||||
challengeFiles: []
|
||||
};
|
||||
|
||||
exports.ENGLISH_CHALLENGE_NO_FILES = ENGLISH_CHALLENGE_NO_FILES;
|
||||
@@ -0,0 +1,16 @@
|
||||
const SIMPLE_TRANSLATION = {
|
||||
'Add your code below this line': {
|
||||
chinese: '(Chinese) Add your code below this line (Chinese)'
|
||||
},
|
||||
'Add your code above this line': {
|
||||
chinese: '(Chinese) Add your code above this line (Chinese)'
|
||||
},
|
||||
'change code below this line': {
|
||||
chinese: '(Chinese) change code below this line (Chinese)'
|
||||
},
|
||||
'change code above this line': {
|
||||
chinese: '(Chinese) change code above this line (Chinese)'
|
||||
}
|
||||
};
|
||||
|
||||
exports.SIMPLE_TRANSLATION = SIMPLE_TRANSLATION;
|
||||
@@ -0,0 +1,40 @@
|
||||
export interface ChallengeFile {
|
||||
contents: string;
|
||||
ext: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Challenge {
|
||||
id: string;
|
||||
title: string;
|
||||
challengeFiles?: ChallengeFile[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CommentDictionary {
|
||||
[comment: string]: {
|
||||
[lang: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function translateComments(
|
||||
text: string,
|
||||
lang: string,
|
||||
dict: CommentDictionary,
|
||||
codeLang: string
|
||||
): { text: string };
|
||||
|
||||
export function translateCommentsInChallenge(
|
||||
challenge: Challenge,
|
||||
lang: string,
|
||||
dict: CommentDictionary
|
||||
): Challenge;
|
||||
|
||||
export function translateGeneric(
|
||||
input: { text: string },
|
||||
config: {
|
||||
knownComments: string[];
|
||||
dict: CommentDictionary;
|
||||
lang: string;
|
||||
}
|
||||
): { text: string };
|
||||
@@ -0,0 +1,98 @@
|
||||
const { cloneDeep } = require('lodash');
|
||||
|
||||
exports.translateComments = (text, lang, dict, codeLang) => {
|
||||
const knownComments = Object.keys(dict);
|
||||
const config = { knownComments, dict, lang };
|
||||
const input = { text };
|
||||
switch (codeLang) {
|
||||
case 'js':
|
||||
case 'jsx':
|
||||
return transMultiline(transInline(input, config), config);
|
||||
case 'html':
|
||||
return transScript(transHTML(transCSS(input, config), config), config);
|
||||
default:
|
||||
return input;
|
||||
}
|
||||
};
|
||||
|
||||
exports.translateCommentsInChallenge = (challenge, lang, dict) => {
|
||||
const challClone = cloneDeep(challenge);
|
||||
if (challClone?.challengeFiles) {
|
||||
challClone.challengeFiles.forEach(challengeFile => {
|
||||
if (challengeFile.contents) {
|
||||
// It cannot be this.translateComments because 'this' does not exist
|
||||
// when imported into an ES module.
|
||||
let { text } = exports.translateComments(
|
||||
challengeFile.contents,
|
||||
lang,
|
||||
dict,
|
||||
challengeFile.ext
|
||||
);
|
||||
challengeFile.contents = text;
|
||||
}
|
||||
});
|
||||
}
|
||||
return challClone;
|
||||
};
|
||||
|
||||
// bare urls could be interpreted as comments, so we have to lookbehind for
|
||||
// http:// or https://
|
||||
function transInline(input, config) {
|
||||
return translateGeneric(input, config, '((?<!https?:)//\\s*)', '(\\s*$)');
|
||||
}
|
||||
|
||||
function transMultiline(input, config) {
|
||||
return translateGeneric(input, 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) {
|
||||
let { text: styleText } = transMultiline({ text: match }, config);
|
||||
text = text.replace(match, styleText);
|
||||
}
|
||||
return { text };
|
||||
}
|
||||
|
||||
function transScript({ text }, config) {
|
||||
const regex = /<script>.*?<\/script>/gms;
|
||||
const matches = text.matchAll(regex);
|
||||
|
||||
for (const [match] of matches) {
|
||||
let { text: scriptText } = transMultiline(
|
||||
transInline({ text: match }, config),
|
||||
config
|
||||
);
|
||||
text = text.replace(match, scriptText);
|
||||
}
|
||||
return { text };
|
||||
}
|
||||
|
||||
function transHTML(input, config) {
|
||||
return translateGeneric(input, 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 if (comment.trim()) {
|
||||
throw `The comment
|
||||
${comment}
|
||||
does not appear in the comment dictionary.
|
||||
When updating or adding a comment it must have the same text in the English challenges and the comment dictionary.
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return { text };
|
||||
}
|
||||
|
||||
exports.translateGeneric = translateGeneric;
|
||||
@@ -0,0 +1,325 @@
|
||||
import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest';
|
||||
import { ENGLISH_CHALLENGE_NO_FILES } from './__fixtures__/challenge-objects';
|
||||
import { SIMPLE_TRANSLATION } from './__fixtures__/mock-comments';
|
||||
import {
|
||||
translateComments,
|
||||
translateCommentsInChallenge,
|
||||
translateGeneric
|
||||
} from '.';
|
||||
|
||||
let logSpy;
|
||||
|
||||
describe('translation parser', () => {
|
||||
beforeEach(() => {
|
||||
logSpy = vi.spyOn(console, 'warn').mockImplementation();
|
||||
});
|
||||
afterEach(() => {
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('translateGeneric', () => {
|
||||
it('returns an object containing translated text', () => {
|
||||
expect.assertions(1);
|
||||
const seed = `// Add your code below this line
|
||||
Add your code above this line `;
|
||||
const transSeed = `// (Chinese) Add your code below this line (Chinese)
|
||||
Add your code above this line `;
|
||||
const knownComments = Object.keys(SIMPLE_TRANSLATION);
|
||||
const config = {
|
||||
knownComments,
|
||||
dict: SIMPLE_TRANSLATION,
|
||||
lang: 'chinese'
|
||||
};
|
||||
const actual = translateGeneric(
|
||||
{ text: seed },
|
||||
config,
|
||||
'((?<!https?:)//\\s*)',
|
||||
'(\\s*$)'
|
||||
);
|
||||
expect(actual.text).toBe(transSeed);
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateCommentsInChallenge', () => {
|
||||
it('returns a clone of the challenge if there are no comments', () => {
|
||||
expect(
|
||||
translateCommentsInChallenge(
|
||||
ENGLISH_CHALLENGE_NO_FILES,
|
||||
'chinese',
|
||||
SIMPLE_TRANSLATION
|
||||
)
|
||||
).toEqual(ENGLISH_CHALLENGE_NO_FILES);
|
||||
});
|
||||
});
|
||||
describe('translateComments', () => {
|
||||
it('replaces single line English comments with their translations', () => {
|
||||
const seed = `// Add your code below this line
|
||||
Add your code above this line `;
|
||||
const transSeed = `// (Chinese) Add your code below this line (Chinese)
|
||||
Add your code above this line `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
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').text
|
||||
).toBe(seed);
|
||||
const seedS = `https:// Add your code below this line
|
||||
Add your code above this line `;
|
||||
expect(
|
||||
translateComments(seedS, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).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 `;
|
||||
const transSeed = `inline comment // (Chinese) Add your code below this line (Chinese)
|
||||
Add your code above this line `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('replaces multiple English comments with their translations', () => {
|
||||
const seed = `inline comment // Add your code below this line
|
||||
// Add your code below this line `;
|
||||
const transSeed = `inline comment // (Chinese) Add your code below this line (Chinese)
|
||||
// (Chinese) Add your code below this line (Chinese) `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('replaces multiline English comments with their translations', () => {
|
||||
const seed = `multiline comment /* Add your code below this line */
|
||||
/* Add your code above this line */ change code below this line `;
|
||||
const transSeed = `multiline comment /* (Chinese) Add your code below this line (Chinese) */
|
||||
/* (Chinese) Add your code above this line (Chinese) */ change code below this line `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('replaces repeated multiline comments with their translations', () => {
|
||||
const seed = `multiline comment /* Add your code below this line */
|
||||
/* Add your code below this line */ change code below this line `;
|
||||
const transSeed = `multiline comment /* (Chinese) Add your code below this line (Chinese) */
|
||||
/* (Chinese) Add your code below this line (Chinese) */ change code below this line `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('ignores empty comments', () => {
|
||||
expect.assertions(1);
|
||||
const seed = '//';
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(seed);
|
||||
});
|
||||
|
||||
it('only replaces text inside comments, not between them', () => {
|
||||
const seed = `multiline comment /* Add your code below this line */
|
||||
/* Add your code above this line */ Add your code below this line /* */ `;
|
||||
const transSeed = `multiline comment /* (Chinese) Add your code below this line (Chinese) */
|
||||
/* (Chinese) Add your code above this line (Chinese) */ Add your code below this line /* */ `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(transSeed);
|
||||
|
||||
const seedTwo = `multiline /* */ Add your code below this line /* */ `;
|
||||
expect(
|
||||
translateComments(seedTwo, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(seedTwo);
|
||||
});
|
||||
|
||||
it('replaces English html comments with their translations', () => {
|
||||
const seed = `<div> <!-- Add your code below this line -->
|
||||
<!-- Add your code above this line --> <span>change code below this line</span> `;
|
||||
const transSeed = `<div> <!-- (Chinese) Add your code below this line (Chinese) -->
|
||||
<!-- (Chinese) Add your code above this line (Chinese) --> <span>change code below this line</span> `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('replaces css comments with their translations', () => {
|
||||
const seed = `<style>
|
||||
/* Add your code below this line */
|
||||
</style>`;
|
||||
const transSeed = `<style>
|
||||
/* (Chinese) Add your code below this line (Chinese) */
|
||||
</style>`;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('replaces multiple css comments with their translations', () => {
|
||||
const seed = `<style>
|
||||
/* Add your code below this line */
|
||||
/* Add your code below this line */
|
||||
</style>`;
|
||||
const transSeed = `<style>
|
||||
/* (Chinese) Add your code below this line (Chinese) */
|
||||
/* (Chinese) Add your code below this line (Chinese) */
|
||||
</style>`;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html')
|
||||
).toEqual({ text: transSeed });
|
||||
});
|
||||
|
||||
it('ignores css comments outside style tags', () => {
|
||||
const seed = `/* Add your code below this line */`;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html').text
|
||||
).toBe(seed);
|
||||
});
|
||||
|
||||
it('ignores css comments between style tags', () => {
|
||||
const seed = `<style>
|
||||
</style>
|
||||
/* Add your code below this line */
|
||||
<style>
|
||||
</style>`;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html').text
|
||||
).toBe(seed);
|
||||
});
|
||||
|
||||
it('only replaces inside English html comments', () => {
|
||||
const seed = `<div> <!-- --> Add your code below this line <!-- -->`;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html').text
|
||||
).toBe(seed);
|
||||
});
|
||||
|
||||
it('replaces English JSX comments with their translations', () => {
|
||||
const seed = `{ /* Add your code below this line */ }
|
||||
{ /* Add your code above this line */ } <span>change code below this line</span> `;
|
||||
const transSeed = `{ /* (Chinese) Add your code below this line (Chinese) */ }
|
||||
{ /* (Chinese) Add your code above this line (Chinese) */ } <span>change code below this line</span> `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'jsx').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('replaces English script comments with their translations', () => {
|
||||
const seed = `<script>
|
||||
// Add your code below this line
|
||||
</script>`;
|
||||
const transSeed = `<script>
|
||||
// (Chinese) Add your code below this line (Chinese)
|
||||
</script>`;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('replaces multiple script comments with their translations', () => {
|
||||
const seed = `<script>
|
||||
/* Add your code below this line */
|
||||
// Add your code below this line
|
||||
</script>`;
|
||||
const transSeed = `<script>
|
||||
/* (Chinese) Add your code below this line (Chinese) */
|
||||
// (Chinese) Add your code below this line (Chinese)
|
||||
</script>`;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('ignores html comments inside JavaScript', () => {
|
||||
const seed = `<div> <!-- Add your code below this line
|
||||
Add your code above this line --> <span>change code below this line</span> `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'js').text
|
||||
).toBe(seed);
|
||||
});
|
||||
|
||||
it('ignores html comments inside jsx', () => {
|
||||
const seed = `<div> <!-- Add your code below this line
|
||||
Add your code above this line --> <span>change code below this line</span> `;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'jsx').text
|
||||
).toBe(seed);
|
||||
});
|
||||
|
||||
it('throws if there is not an exact match (js)', () => {
|
||||
expect.assertions(2);
|
||||
const seedMulti = `/* Add your code below this line
|
||||
Add your code above this line */ <span>change code below this line</span> `;
|
||||
const seedInline = `// Add your code below this line, please`;
|
||||
expect(() =>
|
||||
translateComments(seedMulti, 'chinese', SIMPLE_TRANSLATION, 'js')
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
translateComments(seedInline, 'chinese', SIMPLE_TRANSLATION, 'js')
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('only replaces exact matches (jsx)', () => {
|
||||
expect.assertions(1);
|
||||
const seedMulti = `{ /* Add your code below this line
|
||||
Add your code above this line */ } <span>change code below this line</span> `;
|
||||
expect(() =>
|
||||
translateComments(seedMulti, 'chinese', SIMPLE_TRANSLATION, 'jsx')
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('only replaces exact matches (html)', () => {
|
||||
expect.assertions(1);
|
||||
const seed = `<div> <!-- Add your code below this line
|
||||
Add your code above this line --> <span>change code below this line</span> `;
|
||||
|
||||
expect(() =>
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'html')
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('only translates jsx comments once', () => {
|
||||
const seed = `{ /* Add your code below this line */ }`;
|
||||
const transSeed = `{ /* (Chinese) Add your code below this line (Chinese) */ }`;
|
||||
expect(
|
||||
translateComments(seed, 'chinese', SIMPLE_TRANSLATION, 'jsx').text
|
||||
).toBe(transSeed);
|
||||
});
|
||||
|
||||
it('throws if the comment is not in the dictionary', () => {
|
||||
expect.assertions(6);
|
||||
const seedJSX = `{ /* this is not a comment */ }`;
|
||||
const seedInline = `// this is not a comment `;
|
||||
const seedMulti = `/* this is not a comment */`;
|
||||
const seedCSS = `<style>
|
||||
/* this is not a comment */
|
||||
</style>`;
|
||||
const seedHTML = `<div> <!-- this is not a comment --> `;
|
||||
const seedScript = `<script> // this is not a comment </script>`;
|
||||
|
||||
expect(() =>
|
||||
translateComments(seedJSX, 'chinese', SIMPLE_TRANSLATION, 'jsx')
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
translateComments(seedInline, 'chinese', SIMPLE_TRANSLATION, 'js')
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
translateComments(seedMulti, 'chinese', SIMPLE_TRANSLATION, 'js')
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
translateComments(seedCSS, 'chinese', SIMPLE_TRANSLATION, 'html')
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
translateComments(seedHTML, 'chinese', SIMPLE_TRANSLATION, 'html')
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
translateComments(seedScript, 'chinese', SIMPLE_TRANSLATION, 'html')
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user