Skip to content
Contribute | freeCodeCamp.org

How to Work on Language Curricula

Updated

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

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

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

To create a new block, run:

pnpm run create-new-language-block

The 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

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

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

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

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

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].json
  • client/i18n/locales/english/intro.json

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-block

The 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

Creating the Next Task

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

pnpm run create-next-task

The 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

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

pnpm run insert-task

The 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

To resync task numbering after manual edits, run:

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.

Deleting a Task

To delete a task from a block, run:

pnpm run delete-task

The 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

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

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 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.

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 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).

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

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 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

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).

Contribute | freeCodeCamp.org

Type to search…

↑↓ navigate↵ selectEsc close