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: '你好,世界。',
+ align: 'left'
+ }
+ }
+ ];
+ const result = buildTranscript(commands);
+ expect(result).toBe(
+ '\nNaomi: 你好,世界。\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(
+ '你好,世界。'
+ );
+ });
+
+ 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();
+ });
+});