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.
Task Template
Section titled “Task Template”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 TitledashedName: kebab-case-name-for-taskchallengeType: 19lang: 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 TitledashedName: kebab-case-name-for-taskchallengeType: 22lang: 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 TitledashedName: kebab-case-name-for-taskchallengeType: 31lang: 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 TitledashedName: kebab-case-name-for-quizchallengeType: 8lang: 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 answerModifying Curriculum Outer Layer
Section titled “Modifying Curriculum Outer Layer”To add or edit an outer layer (chapter, module, or block), we use the challenge helper scripts.
Creating a New Block
Section titled “Creating a New Block”To create a new block, run:
pnpm run create-new-language-blockThe script walks you through a series of prompts:
| Prompt | What to provide |
|---|---|
| Superblock | The superblock this block belongs to (e.g., A2 English) |
| Block label | The type of block: practice, learn, quiz, etc. |
| Block dashed name | A unique kebab-case identifier for the block (e.g., en-a2-learn-greetings-and-introductions) |
| Block title | A human-readable title (e.g., Greetings and Introductions) |
| Block layout | The visual layout used to display challenges |
| Chapter | The chapter this block belongs to (you can create a new one) |
| Module | The module within that chapter (you can create a new one) |
| Position | Where in the module this block appears (1-based) |
Block Naming
Section titled “Block Naming”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.
Creating a New Chapter
Section titled “Creating a New Chapter”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:
| Prompt | What to provide |
|---|---|
| Chapter dashed name | A unique kebab-case identifier (e.g., getting-started) |
| Chapter title | A human-readable display title (e.g., Getting Started) |
The chapter is created and the new block is placed inside it.
Creating a New Module
Section titled “Creating a New Module”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:
| Prompt | What to provide |
|---|---|
| Module dashed name | A unique kebab-case identifier (e.g., greetings-and-introductions) |
| Module title | A human-readable display title (e.g., Greetings and Introductions) |
Renaming a Chapter
Section titled “Renaming a Chapter”To rename a chapter, update the chapter’s dashed name and display name in the following files:
curriculum/structure/superblocks/[superblock-name].jsonclient/i18n/locales/english/intro.jsonpackages/shared/src/config/chapters.ts
Renaming a Module
Section titled “Renaming a Module”To rename a module, update the module’s dashed name and display name in the following files:
curriculum/structure/superblocks/[superblock-name].jsonclient/i18n/locales/english/intro.json
Renaming a Block
Section titled “Renaming a Block”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:
pnpm run rename-blockThe script prompts you for:
| Prompt | What to provide |
|---|---|
| Old block dashed name | The current dashed name of the block (must already exist) |
| New display name | The new human-readable title for the block |
| New dashed name | The new kebab-case identifier for the block |
Working with Tasks
Section titled “Working with Tasks”Creating the Next Task
Section titled “Creating the Next Task”To add a new task at the end of a block, run:
pnpm run create-next-taskThe script prompts you for:
| Prompt | What to provide |
|---|---|
| Task challenge type | The 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.
Inserting a Task
Section titled “Inserting a Task”To insert a new task before an existing one, run:
pnpm run insert-taskThe script prompts you for:
| Prompt | What to provide |
|---|---|
| Which task should come AFTER this new one? | Select from the list of existing tasks |
| Task challenge type | The 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.
Reordering Tasks
Section titled “Reordering Tasks”To resync task numbering after manual edits, run:
pnpm run reorder-tasksThe script takes no prompts. It reads the current task order from [block-name].json and updates all task file names and titles to match.
Deleting a Task
Section titled “Deleting a Task”To delete a task from a block, run:
pnpm run delete-taskThe script prompts you for:
| Prompt | What 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.
Adding Scene Assets
Section titled “Adding Scene Assets”Scene assets (backgrounds, characters, and audio) are stored in the cdn repository. To add new assets:
- Create a PR to upload the files to the
cdnrepository- If the asset is an image, upload it to https://github.com/freeCodeCamp/cdn/tree/main/build/curriculum/english/animation-assets/images
- If the asset is audio, upload it to https://github.com/freeCodeCamp/cdn/tree/main/build/curriculum/english/animation-assets/sounds
- Create a PR to add the asset to the main repository:
- Add the new filename or character name 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.snapfile should now be automatically updated with the new asset. Include this change in your PR.
- Add the new filename or character name to
Adding Audio to Quiz Questions
Section titled “Adding Audio to Quiz Questions”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 answerIf the audio file is not available:
- Upload the audio file to the
cdnrepository: 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.snapfile should now be automatically updated with the new asset. Include this change in your PR.
Publishing New Content
Section titled “Publishing New Content”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
isUpcomingChangeproperty tofalsein the block structure file (curriculum/structure/blocks/[block-name].json). - For chapters and modules, remove the
comingSoonproperty from the superblock structure file (curriculum/structure/superblocks/[superblock-name].json).
Working with CJK
Section titled “Working with CJK”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.
CJK Writing Convention
Section titled “CJK Writing Convention”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
BLANKmarkers 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 type | Example |
|---|---|
| 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-blank | BLANK 好 (hǎo),我 (wǒ) BLANK 王华 (wáng huá)。 |
Chinese Input Types
Section titled “Chinese Input Types”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
3afteriproducesǐ). Pressing backspace removes the last typed element (tone number or letter).