verify-translation-prompt.ts 4.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /** Verify that the committed translation prompt renders and parses as documented. */
  2. import { readFileSync } from 'node:fs'
  3. import { join, resolve } from 'node:path'
  4. import {
  5. consumeTranslationResponse,
  6. documentedTranslationPromptPlaceholders,
  7. parseTranslationResponse,
  8. renderTranslationPrompt,
  9. renderTranslationRequest,
  10. renderTranslationResponse,
  11. TRANSLATION_PROMPT_PLACEHOLDERS,
  12. type TranslationExample,
  13. } from './translation-prompt.ts'
  14. const root = resolve(import.meta.dirname, '..')
  15. function read(path: string): string {
  16. return readFileSync(join(root, path), 'utf8')
  17. }
  18. try {
  19. const mode = process.argv[2]
  20. if (mode !== undefined && mode !== '--snapshot') throw new Error(`unsupported argument ${JSON.stringify(mode)}`)
  21. const document = read('docs/i18n/translation-prompt.md')
  22. const terminology = read('docs/i18n/terminology.md')
  23. const examplePaths = [
  24. ['README.md', 'README.zh.md'],
  25. ['docs/development.md', 'docs/development.zh.md'],
  26. ['docs/i18n/README.md', 'docs/i18n/README.zh.md'],
  27. ['docs/i18n/translation-rules.md', 'docs/i18n/translation-rules.zh.md'],
  28. [
  29. '.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md',
  30. '.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md',
  31. ],
  32. ] as const
  33. const examples: TranslationExample[] = examplePaths.map(([english, chinese]) => ({
  34. english: read(english),
  35. chinese: read(chinese),
  36. }))
  37. const sourceDocument = read('scripts/fixtures/translation-prompt/snapshot-note.md')
  38. const recordedResponse = read('scripts/fixtures/translation-prompt/response.txt')
  39. const documented = documentedTranslationPromptPlaceholders(document)
  40. if (documented.join('\n') !== TRANSLATION_PROMPT_PLACEHOLDERS.join('\n')) {
  41. throw new Error(`placeholder table must list exactly: ${TRANSLATION_PROMPT_PLACEHOLDERS.join(', ')}`)
  42. }
  43. const englishInput = { sourceLanguage: 'English' as const, sourceFilename: 'snapshot-note.md', terminology }
  44. const englishSource = renderTranslationPrompt(document, englishInput)
  45. const chineseSource = renderTranslationPrompt(document, {
  46. sourceLanguage: 'Chinese',
  47. sourceFilename: 'snapshot-note.zh.md',
  48. terminology,
  49. })
  50. if (englishSource.includes('{{') || chineseSource.includes('{{')) throw new Error('rendered prompt contains an unresolved placeholder')
  51. if (!englishSource.includes('from English to Chinese')) throw new Error('English-source render does not translate into Chinese')
  52. if (!chineseSource.includes('from Chinese to English')) throw new Error('Chinese-source render does not translate into English')
  53. const example = /```xml\n([\s\S]*?)\n```/.exec(englishSource)?.[1]
  54. if (example === undefined) throw new Error('rendered prompt has no three-section response example')
  55. parseTranslationResponse(example)
  56. const roundTrip = { translation: 'first pass\n\nwith **markdown**', review: '- 无修正', final: 'final text' }
  57. const parsed = parseTranslationResponse(renderTranslationResponse(roundTrip))
  58. if (JSON.stringify(parsed) !== JSON.stringify(roundTrip)) throw new Error('three-section response does not round-trip')
  59. const request = renderTranslationRequest(document, { ...englishInput, sourceDocument, examples })
  60. if (request.targetFilename !== 'snapshot-note.zh.md') throw new Error('English request resolves the wrong target filename')
  61. const expectedRoles = ['system', ...examples.flatMap(() => ['user', 'assistant']), 'user']
  62. if (request.messages.map(message => message.role).join('\n') !== expectedRoles.join('\n')) {
  63. throw new Error('reviewed examples are not assembled as system, example pairs, then source')
  64. }
  65. const consumed = consumeTranslationResponse(recordedResponse, englishInput)
  66. const expectedFinalPrefix = [
  67. '---',
  68. 'layout: doc',
  69. '---',
  70. '',
  71. '# 快照说明',
  72. '',
  73. '[English](snapshot-note.md) | 中文',
  74. '',
  75. ].join('\n')
  76. if (!consumed.final.startsWith(expectedFinalPrefix)) {
  77. throw new Error('recorded frontmatter response does not preserve metadata and receive the canonical target switcher')
  78. }
  79. if (mode === '--snapshot') {
  80. process.stdout.write(`${JSON.stringify({ request, response: consumed }, null, 2)}\n`)
  81. } else {
  82. console.log('verify-translation-prompt: both directions render, reviewed examples assemble, and the consumed response is target-path correct.')
  83. }
  84. } catch (error) {
  85. const message = error instanceof Error ? error.message : String(error)
  86. console.error(`verify-translation-prompt: ${message}`)
  87. process.exit(1)
  88. }