feat(tools): add language block specific properties in helper scripts (#63711)

Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
Krzysztof G.
2025-11-16 11:31:52 +01:00
committed by GitHub
parent 3b7a00a2d4
commit 8ab7c106da
10 changed files with 170 additions and 41 deletions
@@ -30,6 +30,7 @@ import {
updateSimpleSuperblockStructure,
updateChapterModuleSuperblockStructure
} from './helpers/create-project';
import { getLangFromSuperBlock } from './helpers/get-lang-from-superblock';
const helpCategories = [
'English',
@@ -78,13 +79,23 @@ async function createLanguageBlock(
}
await updateIntroJson(superBlock, block, title);
const challengeLang = getLangFromSuperBlock(superBlock);
let challengeId: ObjectID;
if (blockLabel === BlockLabel.quiz) {
challengeId = await createQuizChallenge(block, title, questionCount!);
challengeId = await createQuizChallenge(
block,
title,
questionCount!,
challengeLang
);
blockLayout = BlockLayouts.Link;
} else {
challengeId = await createDialogueChallenge(superBlock, block);
challengeId = await createDialogueChallenge(
superBlock,
block,
challengeLang
);
}
await createMetaJson(
@@ -178,7 +189,8 @@ async function createMetaJson(
async function createDialogueChallenge(
superBlock: SuperBlocks,
block: string
block: string,
challengeLang: string
): Promise<ObjectID> {
const { blockContentDir } = getContentConfig('english') as {
blockContentDir: string;
@@ -188,14 +200,16 @@ async function createDialogueChallenge(
await fs.mkdir(newChallengeDir, { recursive: true });
return createDialogueFile({
projectPath: newChallengeDir + '/'
projectPath: newChallengeDir + '/',
challengeLang: challengeLang
});
}
async function createQuizChallenge(
block: string,
title: string,
questionCount: number
questionCount: number,
challengeLang: string
): Promise<ObjectID> {
const newChallengeDir = path.resolve(
__dirname,
@@ -208,7 +222,8 @@ async function createQuizChallenge(
projectPath: newChallengeDir + '/',
title: title,
dashedName: block,
questionCount: questionCount
questionCount: questionCount,
challengeLang
});
}
@@ -5,18 +5,29 @@ import { getProjectPath } from './helpers/get-project-info';
import { getMetaData, updateMetaData } from './helpers/project-metadata';
import {
createChallengeFile,
getChallenge,
updateTaskMeta,
updateTaskMarkdownFiles
} from './utils';
import { getInputType } from './helpers/get-input-type';
const createNextTask = async () => {
const { challengeType } = await newTaskPrompts();
const meta = getMetaData();
const prevChallengeId =
meta.challengeOrder[meta.challengeOrder.length - 1]?.id;
const challengeLang = prevChallengeId && getChallenge(prevChallengeId)?.lang;
const inputType = await getInputType(challengeType, challengeLang);
// Placeholder title, to be replaced by updateTaskMarkdownFiles
const options = {
title: `Task 0`,
dashedName: 'task-0',
challengeType
challengeType,
...{ ...(challengeLang && { challengeLang }) },
...{ ...(inputType && { inputType }) }
};
const path = getProjectPath();
@@ -29,7 +40,6 @@ const createNextTask = async () => {
createChallengeFile(challengeIdString, challengeText, path);
console.log('Finished creating new task markdown file.');
const meta = getMetaData();
meta.challengeOrder.push({
id: challengeIdString,
title: options.title
@@ -11,19 +11,34 @@ interface ChallengeOptions {
dashedName: string;
challengeType: string;
questionCount?: number;
challengeLang?: string;
inputType?: string;
}
const buildFrontMatter = ({
challengeId,
title,
dashedName,
challengeType
}: ChallengeOptions) => `---
challengeType,
challengeLang,
inputType
}: ChallengeOptions) => {
const langString = challengeLang
? `
lang: ${challengeLang}`
: '';
const inputTypeString = inputType
? `
inputType: ${inputType}`
: '';
return `---
id: ${challengeId.toString()}
title: ${sanitizeTitle(title)}
challengeType: ${challengeType}
dashedName: ${dashedName}
dashedName: ${dashedName}${langString}${inputTypeString}
---`;
};
const buildFrontMatterWithVideo = ({
challengeId,
@@ -0,0 +1,25 @@
import { prompt } from 'inquirer';
import { ChallengeLang } from '../../../shared/config/curriculum';
import { challengeTypes } from '../../../shared/config/challenge-types';
export const getInputType = async (
challengeType: string,
challengeLang?: string
): Promise<string | undefined> => {
const isRequired =
parseInt(challengeType) === challengeTypes.fillInTheBlank &&
challengeLang === ChallengeLang.Chinese;
if (!isRequired) {
return;
}
const inputType = await prompt<{ value: string }>({
name: 'value',
message: 'What input type is challenge using?',
type: 'list',
choices: ['pinyin-tone', 'pinyin-to-hanzi']
});
return inputType.value;
};
@@ -0,0 +1,15 @@
import {
ChallengeLang,
SuperBlocks,
superBlockToSpeechLang
} from '../../../shared/config/curriculum';
export const getLangFromSuperBlock = (
superBlock: SuperBlocks
): ChallengeLang => {
const lang = superBlockToSpeechLang[superBlock];
if (!lang) {
throw new Error(`Missing lang mapping for superBlock: ${superBlock}`);
}
return lang;
};
@@ -2,6 +2,24 @@ import { describe, it, expect } from 'vitest';
import ObjectID from 'bson-objectid';
import { getStepTemplate } from './get-step-template';
const props = {
challengeId: new ObjectID('60d4ebe4801158d1abe1b18f'),
challengeSeeds: [
{
contents: '',
editableRegionBoundaries: [0, 2],
ext: 'html',
head: '',
id: '',
key: 'indexhtml',
name: 'index',
tail: ''
}
],
stepNum: 5,
challengeType: 0
};
// Note: evaluates at highlevel the process, but seedHeads and seedTails could
// be tested if more specifics are needed.
describe('getStepTemplate util', () => {
@@ -35,24 +53,20 @@ Test 1
--fcc-editable-region--
\`\`\`\n`;
const props = {
challengeId: new ObjectID('60d4ebe4801158d1abe1b18f'),
challengeSeeds: [
{
contents: '',
editableRegionBoundaries: [0, 2],
ext: 'html',
head: '',
id: '',
key: 'indexhtml',
name: 'index',
tail: ''
}
],
stepNum: 5,
challengeType: 0
};
expect(getStepTemplate(props)).toEqual(baseOutput);
});
it('should add lang property when challengeLang is passed', () => {
const frontMatter = `---
id: 60d4ebe4801158d1abe1b18f
title: Step 5
challengeType: 0
dashedName: step-5
lang: es
---`;
expect(getStepTemplate({ ...props, challengeLang: 'es' })).match(
new RegExp(`^${frontMatter}`)
);
});
});
@@ -26,6 +26,7 @@ type StepOptions = {
stepNum: number;
challengeType?: number;
isFirstChallenge?: boolean;
challengeLang?: string;
};
export interface ChallengeSeed {
@@ -42,7 +43,8 @@ function getStepTemplate({
challengeSeeds,
stepNum,
challengeType,
isFirstChallenge = false
isFirstChallenge = false,
challengeLang
}: StepOptions): string {
const seedTexts = challengeSeeds
.map(({ contents, ext, editableRegionBoundaries }) => {
@@ -75,12 +77,17 @@ function getStepTemplate({
demoType: onClick`
: '';
const langString = challengeLang
? `
lang: ${challengeLang}`
: '';
return (
`---
id: ${challengeId.toString()}
title: Step ${stepNum}
challengeType: ${challengeType ?? 'placeholder'}
dashedName: step-${stepNum}${demoString}
dashedName: step-${stepNum}${langString}${demoString}
---
# --description--
@@ -5,11 +5,13 @@ import { newTaskPrompts } from './helpers/new-task-prompts';
import { getProjectPath } from './helpers/get-project-info';
import {
createChallengeFile,
getChallenge,
insertChallengeIntoMeta,
updateTaskMeta,
updateTaskMarkdownFiles
} from './utils';
import { getMetaData } from './helpers/project-metadata';
import { getInputType } from './helpers/get-input-type';
const insertChallenge = async () => {
const challenges = getMetaData().challengeOrder;
@@ -22,6 +24,7 @@ const insertChallenge = async () => {
value: id
}))
});
const challengeLang = getChallenge(challengeAfter.id)?.lang;
const indexToInsert = challenges.findIndex(
({ id }) => id === challengeAfter.id
@@ -31,10 +34,13 @@ const insertChallenge = async () => {
const { challengeType } = await newTaskPrompts();
const inputType = await getInputType(challengeType, challengeLang);
const options = {
title: newTaskTitle,
dashedName: 'task-0',
challengeType
challengeType,
...{ ...(challengeLang && { challengeLang }) },
...{ ...(inputType && { inputType }) }
};
const path = getProjectPath();
+16 -6
View File
@@ -22,6 +22,7 @@ interface Options {
projectPath?: string;
challengeSeeds?: ChallengeSeed[];
isFirstChallenge?: boolean;
challengeLang?: string;
}
interface QuizOptions {
@@ -29,6 +30,7 @@ interface QuizOptions {
title: string;
dashedName: string;
questionCount: number;
challengeLang?: string;
}
export async function getAllBlocks() {
@@ -49,7 +51,8 @@ const createStepFile = ({
challengeType,
projectPath = getProjectPath(),
challengeSeeds = [],
isFirstChallenge = false
isFirstChallenge = false,
challengeLang
}: Options): ObjectID => {
const challengeId = new ObjectID();
@@ -58,7 +61,8 @@ const createStepFile = ({
challengeSeeds,
stepNum,
challengeType,
isFirstChallenge
isFirstChallenge,
challengeLang
});
// eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -79,7 +83,8 @@ const createQuizFile = ({
projectPath = getProjectPath(),
title,
dashedName,
questionCount
questionCount,
challengeLang
}: QuizOptions): ObjectID => {
const challengeId = new ObjectID();
const challengeType = challengeTypes.quiz.toString();
@@ -90,7 +95,8 @@ const createQuizFile = ({
challengeType,
title,
dashedName,
questionCount
questionCount,
challengeLang
});
// eslint-disable-next-line @typescript-eslint/no-base-to-string
fs.writeFileSync(`${projectPath}${challengeId.toString()}.md`, quizText);
@@ -98,9 +104,11 @@ const createQuizFile = ({
};
const createDialogueFile = ({
projectPath
projectPath,
challengeLang
}: {
projectPath: string;
challengeLang: string;
}): ObjectID => {
const challengeId = new ObjectID();
const challengeType = challengeTypes.dialogue.toString();
@@ -110,7 +118,8 @@ const createDialogueFile = ({
challengeId,
challengeType,
title: "Dialogue 1: I'm Tom",
dashedName: 'dialogue-1-im-tom'
dashedName: 'dialogue-1-im-tom',
challengeLang
});
// eslint-disable-next-line @typescript-eslint/no-base-to-string
fs.writeFileSync(`${projectPath}${challengeId.toString()}.md`, dialogueText);
@@ -267,6 +276,7 @@ const updateTaskMarkdownFiles = (): void => {
type Challenge = {
challengeType: number;
challengeFiles: ChallengeSeed[];
lang?: string;
};
const getChallenge = (challengeId: string): Challenge => {