diff --git a/client/src/templates/Challenges/components/scene/scene-helpers.test.ts b/client/src/templates/Challenges/components/scene/scene-helpers.test.ts index bcac6738528..8db2fe7dce5 100644 --- a/client/src/templates/Challenges/components/scene/scene-helpers.test.ts +++ b/client/src/templates/Challenges/components/scene/scene-helpers.test.ts @@ -191,5 +191,22 @@ describe('scene-helpers', () => { '\nNaomi: Use
and tags\n' ); }); + + it('should preserve Chinese dialogue with ruby annotations', () => { + const commands: SceneCommand[] = [ + { + character: 'Naomi', + startTime: 1, + dialogue: { + text: '你好(nǐ hǎo)世界(shì jiè)。', + align: 'left' + } + } + ]; + const result = buildTranscript(commands); + expect(result).toBe( + '\nNaomi: 你好(nǐ hǎo)世界(shì jiè)。\n' + ); + }); }); }); diff --git a/client/src/templates/Challenges/components/scene/scene.tsx b/client/src/templates/Challenges/components/scene/scene.tsx index f513f4c6428..7db9ef3ce37 100644 --- a/client/src/templates/Challenges/components/scene/scene.tsx +++ b/client/src/templates/Challenges/components/scene/scene.tsx @@ -393,7 +393,10 @@ export function Scene({ }`} >
{dialogue.label}
-
{dialogue.text}
+
)} diff --git a/tools/challenge-parser/parser/__fixtures__/with-chinese-scene-no-pinyin.md b/tools/challenge-parser/parser/__fixtures__/with-chinese-scene-no-pinyin.md new file mode 100644 index 00000000000..e2e5087723c --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-chinese-scene-no-pinyin.md @@ -0,0 +1,35 @@ +# --description-- + +This challenge has a Chinese scene with plain hanzi (no pinyin). + +# --scene-- + +```json +{ + "setup": { + "background": "company1-reception.png", + "characters": [ + { + "character": "Wang Hua", + "position": { "x": 50, "y": 15, "z": 1.4 }, + "opacity": 0 + } + ], + "audio": { + "filename": "test.mp3", + "startTime": 1 + } + }, + "commands": [ + { + "character": "Wang Hua", + "startTime": 1, + "finishTime": 2, + "dialogue": { + "text": "你好,世界。", + "align": "center" + } + } + ] +} +``` diff --git a/tools/challenge-parser/parser/__fixtures__/with-chinese-scene.md b/tools/challenge-parser/parser/__fixtures__/with-chinese-scene.md new file mode 100644 index 00000000000..dad5573697e --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-chinese-scene.md @@ -0,0 +1,37 @@ +# --description-- + +This challenge has a Chinese scene with hanzi-pinyin pairs. + +# --scene-- + +```json +{ + "setup": { + "background": "company1-reception.png", + "characters": [ + { + "character": "Wang Hua", + "position": { "x": 50, "y": 15, "z": 1.4 }, + "opacity": 0 + } + ], + "audio": { + "filename": "ZH_A1_welcome_hello_world.mp3", + "startTime": 1, + "startTimestamp": 5.18, + "finishTimestamp": 6.71 + } + }, + "commands": [ + { + "character": "Wang Hua", + "startTime": 1, + "finishTime": 2.53, + "dialogue": { + "text": "你好 (nǐ hǎo),世界 (shì jiè)。", + "align": "center" + } + } + ] +} +``` diff --git a/tools/challenge-parser/parser/plugins/add-scene.js b/tools/challenge-parser/parser/plugins/add-scene.js index dc1251b0375..2f87f52b74d 100644 --- a/tools/challenge-parser/parser/plugins/add-scene.js +++ b/tools/challenge-parser/parser/plugins/add-scene.js @@ -1,4 +1,8 @@ const { getSection } = require('./utils/get-section'); +const { + createMdastToHtml, + parseHanziPinyinPairs +} = require('./utils/i18n-stringify'); function plugin() { return transformer; @@ -17,6 +21,47 @@ function plugin() { // throws if we can't parse it. const sceneJson = JSON.parse(sceneNodes[0].value); + + // Convert hanzi-pinyin pairs to HTML in dialogue text + if (sceneJson.commands) { + const toHtml = createMdastToHtml(file.data.lang); + + sceneJson.commands = sceneJson.commands.map(command => { + if ( + command.dialogue && + command.dialogue.text && + parseHanziPinyinPairs(command.dialogue.text).length > 0 + ) { + // Wrap text in inlineCode node so the Chinese handler can process it. + // The paragraph wrapper is required by mdastToHTML's structure. + const nodes = [ + { + type: 'paragraph', + children: [ + { + type: 'inlineCode', + value: command.dialogue.text + } + ] + } + ]; + + const html = toHtml(nodes); + // Remove the

wrapper tags, keeping only the inner ruby elements + const innerHtml = html.replace(/^

|<\/p>$/g, ''); + + return { + ...command, + dialogue: { + ...command.dialogue, + text: innerHtml + } + }; + } + return command; + }); + } + file.data.scene = sceneJson; } } diff --git a/tools/challenge-parser/parser/plugins/add-scene.test.js b/tools/challenge-parser/parser/plugins/add-scene.test.js new file mode 100644 index 00000000000..c7307bd52b8 --- /dev/null +++ b/tools/challenge-parser/parser/plugins/add-scene.test.js @@ -0,0 +1,67 @@ +import { describe, beforeAll, beforeEach, it, expect } from 'vitest'; +import parseFixture from '../__fixtures__/parse-fixture'; +import addScene from './add-scene'; + +describe('add-scene', () => { + let sceneAST, chineseSceneAST, chineseSceneNoPinyinAST; + let file; + + beforeAll(async () => { + sceneAST = await parseFixture('scene.md'); + chineseSceneAST = await parseFixture('with-chinese-scene.md'); + chineseSceneNoPinyinAST = await parseFixture( + 'with-chinese-scene-no-pinyin.md' + ); + }); + + beforeEach(() => { + file = { data: { lang: 'en' } }; + }); + + it('should add scene data to file when scene section exists', () => { + const plugin = addScene(); + plugin(sceneAST, file); + + expect(file.data.scene).toBeDefined(); + expect(file.data.scene.setup.background).toBe('company2-center.png'); + expect(file.data.scene.commands).toHaveLength(3); + }); + + it('should preserve dialogue text for non-Chinese scenes', () => { + const plugin = addScene(); + plugin(sceneAST, file); + + expect(file.data.scene.commands[1].dialogue.text).toBe( + "I'm Maria, the team lead." + ); + expect(file.data.scene.commands[1].dialogue.text).not.toContain(''); + }); + + it('should convert Chinese hanzi-pinyin pairs to ruby HTML', () => { + file.data.lang = 'zh-CN'; + const plugin = addScene(); + plugin(chineseSceneAST, file); + + const dialogueText = file.data.scene.commands[0].dialogue.text; + expect(dialogueText).toBe( + '你好(nǐ hǎo)世界(shì jiè)。' + ); + }); + + it('should not convert Hanzi-only to ruby HTML', () => { + file.data.lang = 'zh-CN'; + const plugin = addScene(); + plugin(chineseSceneNoPinyinAST, file); + + expect(file.data.scene.commands[0].dialogue.text).toBe('你好,世界。'); + expect(file.data.scene.commands[0].dialogue.text).not.toContain(''); + }); + + it('should handle commands without dialogue', () => { + const plugin = addScene(); + plugin(sceneAST, file); + + expect(file.data.scene.commands[0].dialogue).toBeUndefined(); + expect(file.data.scene.commands[2].dialogue).toBeUndefined(); + }); +});