mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-04-30 16:01:14 -04:00
feat(challenge-parser,client): display Chinese dialogue with ruby annotations (#64235)
This commit is contained in:
@@ -191,5 +191,22 @@ describe('scene-helpers', () => {
|
||||
'\n<strong>Naomi</strong>: Use <div> and <span> tags\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve Chinese dialogue with ruby annotations', () => {
|
||||
const commands: SceneCommand[] = [
|
||||
{
|
||||
character: 'Naomi',
|
||||
startTime: 1,
|
||||
dialogue: {
|
||||
text: '<ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>,<ruby>世界<rp>(</rp><rt>shì jiè</rt><rp>)</rp></ruby>。',
|
||||
align: 'left'
|
||||
}
|
||||
}
|
||||
];
|
||||
const result = buildTranscript(commands);
|
||||
expect(result).toBe(
|
||||
'\n<strong>Naomi</strong>: <ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>,<ruby>世界<rp>(</rp><rt>shì jiè</rt><rp>)</rp></ruby>。\n'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -393,7 +393,10 @@ export function Scene({
|
||||
}`}
|
||||
>
|
||||
<div className='scene-dialogue-label'>{dialogue.label}</div>
|
||||
<div className='scene-dialogue-text'>{dialogue.text}</div>
|
||||
<div
|
||||
className='scene-dialogue-text'
|
||||
dangerouslySetInnerHTML={{ __html: dialogue.text }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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 <p> wrapper tags, keeping only the inner ruby elements
|
||||
const innerHtml = html.replace(/^<p>|<\/p>$/g, '');
|
||||
|
||||
return {
|
||||
...command,
|
||||
dialogue: {
|
||||
...command.dialogue,
|
||||
text: innerHtml
|
||||
}
|
||||
};
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
||||
file.data.scene = sceneJson;
|
||||
}
|
||||
}
|
||||
|
||||
67
tools/challenge-parser/parser/plugins/add-scene.test.js
Normal file
67
tools/challenge-parser/parser/plugins/add-scene.test.js
Normal file
@@ -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('<ruby>');
|
||||
});
|
||||
|
||||
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(
|
||||
'<ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>,<ruby>世界<rp>(</rp><rt>shì jiè</rt><rp>)</rp></ruby>。'
|
||||
);
|
||||
});
|
||||
|
||||
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('<ruby>');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user