fix(i18n): harden prompt response handling
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
<translation>
|
||||
---
|
||||
layout: doc
|
||||
---
|
||||
|
||||
# 快照说明
|
||||
|
||||
agent(智能体)执行一个步骤。
|
||||
@@ -9,6 +13,10 @@ agent(智能体)执行一个步骤。
|
||||
</review>
|
||||
|
||||
<final>
|
||||
---
|
||||
layout: doc
|
||||
---
|
||||
|
||||
# 快照说明
|
||||
|
||||
agent(智能体)执行一个步骤。
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
layout: doc
|
||||
---
|
||||
|
||||
# Snapshot note
|
||||
|
||||
The agent performs one step.
|
||||
|
||||
@@ -48,13 +48,13 @@
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "# Snapshot note\n\nThe agent performs one step.\n"
|
||||
"content": "---\nlayout: doc\n---\n\n# Snapshot note\n\nThe agent performs one step.\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"translation": "# 快照说明\n\nagent(智能体)执行一个步骤。",
|
||||
"translation": "---\nlayout: doc\n---\n\n# 快照说明\n\nagent(智能体)执行一个步骤。",
|
||||
"review": "- 无修正",
|
||||
"final": "# 快照说明\n\n[English](snapshot-note.md) | 中文\n\nagent(智能体)执行一个步骤。\n"
|
||||
"final": "---\nlayout: doc\n---\n\n# 快照说明\n\n[English](snapshot-note.md) | 中文\n\nagent(智能体)执行一个步骤。\n"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,17 @@ describe('translation prompt rendering', () => {
|
||||
expect(() => renderTranslationPrompt(missing, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })).toThrow(/required placeholder/)
|
||||
})
|
||||
|
||||
it('rejects unmatched placeholder delimiters', () => {
|
||||
for (const delimiter of ['{{', '}}']) {
|
||||
const malformed = document.replace('Your task is to translate', `Your task ${delimiter} is to translate`)
|
||||
expect(() => renderTranslationPrompt(malformed, {
|
||||
sourceLanguage: 'English',
|
||||
sourceFilename: 'guide.md',
|
||||
terminology,
|
||||
})).toThrow(/malformed placeholder syntax/)
|
||||
}
|
||||
})
|
||||
|
||||
it('assembles bare few-shot turns before the real source document', () => {
|
||||
const request = renderTranslationRequest(document, {
|
||||
sourceLanguage: 'English',
|
||||
@@ -127,6 +138,46 @@ describe('translation response sections', () => {
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
it('preserves YAML frontmatter before inserting the target switcher', () => {
|
||||
const response = renderTranslationResponse({
|
||||
translation: '# 指南\n\n初稿。',
|
||||
review: '- 无修正',
|
||||
final: [
|
||||
'---',
|
||||
'layout: home',
|
||||
'---',
|
||||
'',
|
||||
'# 指南',
|
||||
'',
|
||||
'定稿。',
|
||||
].join('\n'),
|
||||
})
|
||||
expect(consumeTranslationResponse(response, { sourceLanguage: 'English', sourceFilename: 'guide.md' }).final).toBe([
|
||||
'---',
|
||||
'layout: home',
|
||||
'---',
|
||||
'',
|
||||
'# 指南',
|
||||
'',
|
||||
'[English](guide.md) | 中文',
|
||||
'',
|
||||
'定稿。',
|
||||
'',
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
it('rejects unterminated YAML frontmatter before the target H1', () => {
|
||||
const response = renderTranslationResponse({
|
||||
translation: '# 指南\n\n初稿。',
|
||||
review: '- 无修正',
|
||||
final: '---\nlayout: home\n\n# 指南\n\n定稿。',
|
||||
})
|
||||
expect(() => consumeTranslationResponse(response, {
|
||||
sourceLanguage: 'English',
|
||||
sourceFilename: 'guide.md',
|
||||
})).toThrow(/unterminated YAML frontmatter/)
|
||||
})
|
||||
|
||||
it('rejects a source filename that contradicts the translation direction', () => {
|
||||
expect(() => renderTranslationPrompt(document, {
|
||||
sourceLanguage: 'Chinese',
|
||||
|
||||
@@ -119,6 +119,10 @@ export function renderTranslationPrompt(document: string, input: TranslationProm
|
||||
terminology: input.terminology,
|
||||
}
|
||||
const template = extractTranslationPrompt(document)
|
||||
const placeholderFreeTemplate = template.replace(PLACEHOLDER, '')
|
||||
if (placeholderFreeTemplate.includes('{{') || placeholderFreeTemplate.includes('}}')) {
|
||||
throw new Error('translation prompt: template contains malformed placeholder syntax')
|
||||
}
|
||||
const names = [...template.matchAll(PLACEHOLDER)].map(match => match[1] ?? '')
|
||||
const unknown = names.filter(name => !TRANSLATION_PROMPT_PLACEHOLDERS.includes(name as TranslationPromptPlaceholder))
|
||||
if (unknown.length > 0) throw new Error(`translation prompt: unsupported placeholder(s): ${[...new Set(unknown)].join(', ')}`)
|
||||
@@ -215,16 +219,24 @@ export function parseTranslationResponse(text: string): TranslationResponse {
|
||||
function correctLanguageSwitcher(markdown: string, switcher: string): string {
|
||||
const lines = markdown.replaceAll('\r\n', '\n').split('\n')
|
||||
while (lines.at(-1) === '') lines.pop()
|
||||
if (!/^#\s+\S/.test(lines[0] ?? '')) {
|
||||
|
||||
let headingIndex = 0
|
||||
if (lines[0] === '---') {
|
||||
const frontmatterEnd = lines.indexOf('---', 1)
|
||||
if (frontmatterEnd === -1) throw new Error('translation response: final document has unterminated YAML frontmatter')
|
||||
headingIndex = frontmatterEnd + 1
|
||||
while (lines[headingIndex] === '') headingIndex++
|
||||
}
|
||||
if (!/^#\s+\S/.test(lines[headingIndex] ?? '')) {
|
||||
throw new Error('translation response: final document must start with an H1 heading')
|
||||
}
|
||||
|
||||
let contentStart = 1
|
||||
let contentStart = headingIndex + 1
|
||||
while (lines[contentStart] === '') contentStart++
|
||||
if (LANGUAGE_SWITCHER.test(lines[contentStart] ?? '')) contentStart++
|
||||
while (lines[contentStart] === '') contentStart++
|
||||
|
||||
const output = [lines[0] as string, '', switcher]
|
||||
const output = [...lines.slice(0, headingIndex), lines[headingIndex] as string, '', switcher]
|
||||
const content = lines.slice(contentStart)
|
||||
if (content.length > 0) output.push('', ...content)
|
||||
return `${output.join('\n')}\n`
|
||||
|
||||
@@ -71,8 +71,18 @@ try {
|
||||
throw new Error('reviewed examples are not assembled as system, example pairs, then source')
|
||||
}
|
||||
const consumed = consumeTranslationResponse(recordedResponse, englishInput)
|
||||
if (consumed.final.split('\n')[2] !== '[English](snapshot-note.md) | 中文') {
|
||||
throw new Error('recorded new-pair response does not receive the canonical target switcher')
|
||||
const expectedFinalPrefix = [
|
||||
'---',
|
||||
'layout: doc',
|
||||
'---',
|
||||
'',
|
||||
'# 快照说明',
|
||||
'',
|
||||
'[English](snapshot-note.md) | 中文',
|
||||
'',
|
||||
].join('\n')
|
||||
if (!consumed.final.startsWith(expectedFinalPrefix)) {
|
||||
throw new Error('recorded frontmatter response does not preserve metadata and receive the canonical target switcher')
|
||||
}
|
||||
|
||||
if (mode === '--snapshot') {
|
||||
|
||||
Reference in New Issue
Block a user