Skip to content

How to Work on Language Curricula

Language curricula follow a five-level hierarchy:

  • Superblock — The top-level course (e.g., A2 English for Developers)
  • Chapter — A broad thematic group within a superblock
  • Module — A focused topic within a chapter
  • Block — A set of tasks covering a specific lesson topic
  • Task — An individual challenge within a block (multiple-choice, fill-in-the-blank, quiz, etc.)

For file structure, see Curriculum File Structure.

Tasks are written in markdown with frontmatter metadata. The boilerplate is automatically generated when you create a new task with the helper scripts.

The following examples show the possible sections for each task type. Most tasks only use a subset of these sections.

Multiple-Choice Question (Challenge Type 19)
---
id: Unique identifier (alphanumerical, MongoDB_id)
title: Task Title
dashedName: kebab-case-name-for-task
challengeType: 19
lang: language code (e.g., `en-US`, `es`, `zh-CN`)
videoId: YouTube video ID (if applicable)
---
# --description--
Task description text.
# --transcript--
Transcript of the video, should only be used if the task has a video (`videoId` is defined).
# --instructions--
Task instructions text, in markdown.
# --questions--
## --text--
Question text.
## --answers--
First option
### --audio-id--
Audio ID for this option (the name of the audio file, without the extension)
### --feedback--
Feedback shown when campers guess this answer
---
Second option
### --audio-id--
Audio ID for this option
### --feedback--
Feedback shown when campers guess this answer
---
Third option
### --feedback--
Feedback shown when campers guess this answer
---
Fourth option
## --video-solution--
The number for the correct answer goes here (1-based).
# --explanation--
Explanation text.
# --scene--
```json
// # --scene-- can only consist of a single json object
{
// Setup the scene. Properties not marked optional are required.
"setup": {
// Background file to start the scene. A list of scene asset filenames can be found here: https://github.com/freeCodeCamp/cdn/pull/233/files
"background": "company2-center.png",
// Array of all characters that will appear in the scene
"characters": [
{
// Name of character. See list of available characters in scene-assets.tsx
"character": "Maria",
// Where to start the character. Maria will start off screen to the left
"position": { "x": -25, "y": 0, "z": 1 }
},
{
"character": "Tom",
// Tom will start 70% from the left of the screen and 1.5 times regular size
"position": { "x": 70, "y": 0, "z": 1.5 },
// Optional, defaults to 1. Tom will start invisible
"opacity": 0
}
],
"audio": {
// Audio filename
"filename": "1.1-1.mp3",
// Seconds after the scene starts before the audio starts playing
"startTime": 1.3,
// Optional. Timestamp of the audio file where it starts playing from.
"startTimestamp": 0,
// Optional. Timestamp of the audio file where is stops playing. If these two aren't used, the whole audio file will play.
"finishTimestamp": 8.4
},
// Optional, defaults to false. Use this for the long dialogues. It stops the accessibility icon from showing which gives campers the option to show or hide the dialogue text
"alwaysShowDialogue": true
},
// Array of commands that make up the scene
"commands": [
{
// Character that will have an action for this command
"character": "Maria",
// Optional, defaults to previous value. Maria will move to 25% from the left of the screen. The movement takes 0.5 seconds
"position": { "x": 25, "y": 0, "z": 1 },
// When the command will start. Zero seconds after the camper presses play
"startTime": 0
},
{
"character": "Tom",
// Optional, defaults to previous value. Tom will fade into view. The transition take 0.5 seconds. Movement and Opacity transitions take 0.5 seconds
"opacity": 1,
// Tom will fade into view 0.5 seconds into the scene (immediately after Maria finishes moving on screen)
"startTime": 0.5
},
{
"character": "Maria",
// When the command starts: Maria will start saying this line 1.3 seconds into the scene. Note that this is the same time as the audio.startTime above. It doesn't have to match that (maybe there's a pause at the beginning of the audio or something)
"startTime": 1.3,
// The character will stop moving their mouth at the finishTime
"finishTime": 4.95,
"dialogue": {
// Text that will appear if the dialogue is visible
"text": "Hello! You're the new graphic designer, right? I'm Maria, the team lead.",
// Where the dialogue text will be aligned. Can be 'left', 'center', or 'right'
"align": "left"
}
},
{
// background will change to this at 5.4 seconds into the scene
"background": "company2-breakroom.png",
"character": "Tom",
"startTime": 5.4,
"finishTime": 9.4,
"dialogue": {
"text": "Hi, that's right! I'm Tom McKenzie. It's a pleasure to meet you.",
// Tom's text will be aligned to the right since he is on the right side of the screen
"align": "right"
}
},
{
"character": "Tom",
// Tom will fade to 0 opacity
"opacity": 0,
// I like to move characters off screen or fade them 0.5 second after the last talking command
"startTime": 9.9
},
{
"character": "Maria",
// Maria will slide back off the screen to the left
"position": { "x": -25, "y": 0, "z": 1 },
// The animation will stop playing 0.5 seconds after the 'finishTime' of the last command - or 0.5 seconds after 'startTime' if 'finishTime' isn't there.
"startTime": 10.4
}
]
}
```
Fill-in-the-Blank (Challenge Type 22)
---
id: Unique identifier (alphanumerical, MongoDB_id)
title: Task Title
dashedName: kebab-case-name-for-task
challengeType: 22
lang: language code (e.g., `en-US`, `es`, `zh-CN`)
---
# --description--
Task description text.
# --instructions--
Task instructions text, in markdown.
# --fillInTheBlank--
## --sentence--
`The sentence with BLANK markers`
## --blanks--
`First blank answer`
### --feedback--
Feedback for the first blank
---
`Second blank answer`
### --feedback--
Feedback for the second blank
# --explanation--
Explanation text.
# --scene--
```json
{
"setup": {
"background": "company2-center.png",
"characters": [
{
"character": "Maria",
"position": { "x": 50, "y": 25, "z": 1.5 },
"opacity": 0
}
],
"audio": {
"filename": "1.1-1.mp3",
"startTime": 1,
"startTimestamp": 0,
"finishTimestamp": 4.5
}
},
"commands": [
{
"character": "Maria",
"opacity": 1,
"startTime": 0
},
{
"character": "Maria",
"startTime": 1,
"finishTime": 3.5,
"dialogue": {
"text": "Hello! Nice to meet you.",
"align": "center"
}
},
{
"character": "Maria",
"opacity": 0,
"startTime": 4
}
]
}
```
Review (Challenge Type 31)
---
id: Unique identifier (alphanumerical, MongoDB_id)
title: Task Title
dashedName: kebab-case-name-for-task
challengeType: 31
lang: language code (e.g., `en-US`, `es`, `zh-CN`)
---
# --description--
Review content in markdown.
# --assignments--
This will show a checkbox that campers have to check before completing a task.
---
This will show another checkbox that campers have to check before completing a challenge.
Quiz (Challenge Type 8)
---
id: Unique identifier (alphanumerical, MongoDB_id)
title: Quiz Title
dashedName: kebab-case-name-for-quiz
challengeType: 8
lang: language code (e.g., `en-US`, `es`, `zh-CN`)
---
# --description--
Quiz description text.
# --quizzes--
## --quiz--
### --question--
#### --text--
Question text.
#### --audio--
```json
{
"audio": {
"filename": "audio-filename.mp3",
"startTimestamp": 0,
"finishTimestamp": 4.5
},
"transcript": [
{
"character": "Character Name",
"text": "Audio transcript text."
}
]
}
```
#### --distractors--
First distractor option
---
Second distractor option
---
Third distractor option
#### --answer--
The correct answer

To add or edit an outer layer (chapter, module, or block), we use the challenge helper scripts.

To create a new block, run:

Terminal window
pnpm run create-new-language-block

The script walks you through a series of prompts:

PromptWhat to provide
SuperblockThe superblock this block belongs to (e.g., A2 English)
Block labelThe type of block: practice, learn, quiz, etc.
Block dashed nameA unique kebab-case identifier for the block (e.g., en-a2-learn-greetings-and-introductions)
Block titleA human-readable title (e.g., Greetings and Introductions)
Block layoutThe visual layout used to display challenges
ChapterThe chapter this block belongs to (you can create a new one)
ModuleThe module within that chapter (you can create a new one)
PositionWhere in the module this block appears (1-based)

Block names for language curricula follow an automatic prefix based on the superblock and block label. For example, a learn block in A2 English gets the prefix en-a2-learn-. When prompted, enter only the unique part of the name after the prefix.

Chapters are selected from a list during the block creation flow. If the chapter you need doesn’t exist yet:

  • Run pnpm run create-new-language-block
  • Choose — Create new chapter — when prompted

You will then be asked for:

PromptWhat to provide
Chapter dashed nameA unique kebab-case identifier (e.g., getting-started)
Chapter titleA human-readable display title (e.g., Getting Started)

The chapter is created and the new block is placed inside it.

Modules are selected from a list scoped to the chosen chapter during the block creation flow. If the module you need doesn’t exist yet:

  • Run pnpm run create-new-language-block
  • Choose — Create new module — when prompted

You will be asked for:

PromptWhat to provide
Module dashed nameA unique kebab-case identifier (e.g., greetings-and-introductions)
Module titleA human-readable display title (e.g., Greetings and Introductions)

To rename a chapter, update the chapter’s dashed name and display name in the following files:

  • curriculum/structure/superblocks/[superblock-name].json
  • client/i18n/locales/english/intro.json
  • packages/shared/src/config/chapters.ts

To rename a module, update the module’s dashed name and display name in the following files:

  • curriculum/structure/superblocks/[superblock-name].json
  • client/i18n/locales/english/intro.json

The rename-block script renames an existing block and updates every file that references it, including the block metadata, the challenge directory, all superblock structures, and the locale intro file.

To rename a block, run:

Terminal window
pnpm run rename-block

The script prompts you for:

PromptWhat to provide
Old block dashed nameThe current dashed name of the block (must already exist)
New display nameThe new human-readable title for the block
New dashed nameThe new kebab-case identifier for the block

To add a new task at the end of a block, run:

Terminal window
pnpm run create-next-task

The script prompts you for:

PromptWhat to provide
Task challenge typeThe type of challenge: multipleChoice, fillInTheBlank, or generic
Input type (Chinese fill-in-the-blank only)pinyin-tone or pinyin-to-hanzi

After running, the script creates the task file, appends it to the [block-name].json file, and renumbers all tasks in the block.

To insert a new task before an existing one, run:

Terminal window
pnpm run insert-task

The script prompts you for:

PromptWhat to provide
Which task should come AFTER this new one?Select from the list of existing tasks
Task challenge typeThe type of challenge: multipleChoice, fillInTheBlank, or generic
Input type (Chinese fill-in-the-blank only)pinyin-tone or pinyin-to-hanzi

After running, the script creates the task file, inserts it at the correct position in the [block-name].json file, and renumbers all tasks in the block.

To resync task numbering after manual edits, run:

Terminal window
pnpm run reorder-tasks

The script takes no prompts. It reads the current task order from [block-name].json and updates all task file names and titles to match.

To delete a task from a block, run:

Terminal window
pnpm run delete-task

The script prompts you for:

PromptWhat to provide
Which challenge should be deleted?Select from the list of existing tasks

After running, the script deletes the task file, removes it from [block-name].json, and renumbers the remaining tasks.

Scene assets (backgrounds, characters, and audio) are stored in the cdn repository. To add new assets:

In quizzes (challenge type 8), a question can have an optional audio clip. Use the following template to add audio to a quiz question:

Quiz Question with Audio Template
### --question--
#### --text--
Question text goes here.
#### --audio--
```json
{
"audio": {
"filename": "audio-file.mp3",
"startTimestamp": 0,
"finishTimestamp": 4.5
},
"transcript": [
{
"character": "Character Name",
"text": "Transcript text."
}
]
}
```
#### --distractors--
Distractor option 1
---
Distractor option 2
---
Distractor option 3
#### --answer--
Correct answer

If the audio file is not available:

  • Upload the audio file to the cdn repository: https://github.com/freeCodeCamp/cdn/tree/main/build/curriculum/english/animation-assets/sounds
  • Add the filename to curriculum/schema/scene-assets.js
  • Update the snapshot test by running pnpm --filter @freecodecamp/curriculum exec vitest run schema/challenge-schema.test.mjs -u
  • The curriculum/schema/__snapshots__/challenge-schema.test.mjs.snap file should now be automatically updated with the new asset. Include this change in your PR.

When a chapter, module, or block is created via the script, it is hidden by default, meaning it won’t be visible on the production site until you choose to publish it.

When you’re ready to publish:

  • For blocks, change the isUpcomingChange property to false in the block structure file (curriculum/structure/blocks/[block-name].json).
  • For chapters and modules, remove the comingSoon property from the superblock structure file (curriculum/structure/superblocks/[superblock-name].json).

CJK language curricula (Chinese, Japanese, Korean) use a base text — annotation pair convention to link source characters to their pronunciation guide. For example:

  • 你好 (nǐ hǎo)
  • こんにちは (konnichiwa) or こ (ko) ん (n) に (ni) ち (chi) は (wa)
  • 안녕하세요 (annyeonghaseyo) or 안 (an) 녕 (nyeong) 하 (ha) 세 (se) 요 (yo)

The challenge parser converts these pairs into HTML <ruby> elements for display.

The decision of where to split segments is ultimately up to the author, but the following are general guidelines:

  • Punctuation marks: place the annotation before the punctuation
  • English words: do not annotate them
  • BLANK markers in fill-in-the-blank tasks: do not annotate them
  • Lowercase annotations: always use lowercase for annotations, even if the base text is a proper noun or is the first word of a sentence

Here are some examples:

Content typeExample
Static text (basic)我叫王华 (wǒ jiào wáng huá)。
Static text (with internal punctuation)你好 (nǐ hǎo),我是王华 (wǒ shì wáng huá)。
Static text (with English)我是 (wǒ shì) UI 设计师 (shè jì shī)。
Fill-in-the-blankBLANK 好 (hǎo),我 (wǒ) BLANK 王华 (wáng huá)。

Chinese fill-in-the-blank tasks use one of two input types. You select the input type when creating a task with create-next-task or insert-task.

  • Pinyin-to-Hanzi: The learner types Pinyin with tone numbers (1–5). When a correctly typed syllable is entered, it converts to the corresponding Chinese character. Pressing backspace on a Chinese character reverts it to Pinyin and removes the last typed element (tone number or letter).

  • Pinyin Tone: The learner types Pinyin with tone numbers (1–5). Tone numbers are converted to tone marks in real time (e.g., typing 3 after i produces ǐ). Pressing backspace removes the last typed element (tone number or letter).