diff --git a/src/content/docs/how-to-work-on-language-curricula.mdx b/src/content/docs/how-to-work-on-language-curricula.mdx new file mode 100644 index 00000000..48757cc3 --- /dev/null +++ b/src/content/docs/how-to-work-on-language-curricula.mdx @@ -0,0 +1,662 @@ +--- +title: How to Work on Language Curricula +sidebar: + label: Work on Language Curricula +--- + +import { Steps } from '@astrojs/starlight/components'; +import { Aside } from '@astrojs/starlight/components'; + +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](/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: + +```bash +pnpm run create-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`) | +| Help category | The support category for this block's forum posts | +| 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-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-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: + +```bash +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: + +```bash +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: + +```bash +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: + +```bash +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: + +```bash +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: + +- Create a PR to upload the files to the `cdn` repository + - 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.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 `` 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). diff --git a/src/sidebar.ts b/src/sidebar.ts index 1ba38b58..f457b15b 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -17,6 +17,7 @@ const sidebar = [ 'how-to-work-on-workshops', 'how-to-setup-freecodecamp-mobile-app-locally', 'how-to-work-on-localized-client-webapp', + 'how-to-work-on-language-curricula', 'how-to-add-playwright-tests', 'how-to-work-on-the-docs-site', 'how-to-open-a-pull-request',