1
0

translation-pairing-record.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /** Canonical paths, parsing, and rendering for bilingual pairing records. */
  2. import { basename } from 'node:path'
  3. /** The three repository-relative paths that form one bilingual pair. */
  4. export interface TranslationPairPaths {
  5. /** English document path. */
  6. source: string
  7. /** Simplified Chinese document path. */
  8. zh: string
  9. /** Generated consistency-record path. */
  10. meta: string
  11. }
  12. /** The two content hashes recorded for a bilingual pair. */
  13. export interface TranslationPairingRecord {
  14. /** Git blob hash of the English document. */
  15. sourceHash: string
  16. /** Git blob hash of the Simplified Chinese document. */
  17. zhHash: string
  18. }
  19. const META_LINE = /^([^:#]+\.md): ([0-9a-f]{40})$/
  20. /**
  21. * Derive the counterpart and consistency-record paths from an English document.
  22. *
  23. * @param source - Repository-relative English Markdown path.
  24. * @returns The complete three-path pair.
  25. */
  26. export function translationPairPaths(source: string): TranslationPairPaths {
  27. if (!source.endsWith('.md') || source.endsWith('.zh.md')) {
  28. throw new Error(`expected an English Markdown path, received ${JSON.stringify(source)}`)
  29. }
  30. return {
  31. source,
  32. zh: source.replace(/\.md$/, '.zh.md'),
  33. meta: source.replace(/\.md$/, '.i18n.yaml'),
  34. }
  35. }
  36. /**
  37. * Derive one pair from its consistency-record path.
  38. *
  39. * @param meta - Repository-relative `foo.i18n.yaml` path.
  40. * @returns The complete three-path pair.
  41. */
  42. export function translationPairPathsFromMeta(meta: string): TranslationPairPaths {
  43. if (!meta.endsWith('.i18n.yaml')) {
  44. throw new Error(`expected a bilingual consistency-record path, received ${JSON.stringify(meta)}`)
  45. }
  46. return translationPairPaths(meta.replace(/\.i18n\.yaml$/, '.md'))
  47. }
  48. /**
  49. * Parse a consistency record for its expected sibling names.
  50. *
  51. * @param content - Complete sidecar text.
  52. * @param paths - Expected sibling paths.
  53. * @returns The two hashes, or `undefined` for malformed, duplicate, or unexpected keys.
  54. */
  55. export function parseTranslationPairingRecord(
  56. content: string,
  57. paths: TranslationPairPaths,
  58. ): TranslationPairingRecord | undefined {
  59. const hashes = new Map<string, string>()
  60. for (const line of content.split('\n')) {
  61. if (line === '' || line.startsWith('#')) continue
  62. const match = META_LINE.exec(line)
  63. if (!match?.[1] || !match[2] || hashes.has(match[1])) return undefined
  64. hashes.set(match[1], match[2])
  65. }
  66. const sourceHash = hashes.get(basename(paths.source))
  67. const zhHash = hashes.get(basename(paths.zh))
  68. if (hashes.size !== 2 || sourceHash === undefined || zhHash === undefined) return undefined
  69. return { sourceHash, zhHash }
  70. }
  71. /**
  72. * Render the canonical consistency record for a pair.
  73. *
  74. * @param paths - Pair paths written into the record and its recovery command.
  75. * @param record - Confirmed content hashes.
  76. * @returns Canonical YAML text with exactly one trailing newline.
  77. */
  78. export function renderTranslationPairingRecord(
  79. paths: TranslationPairPaths,
  80. record: TranslationPairingRecord,
  81. ): string {
  82. return [
  83. '# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each',
  84. '# side as of the last confirmed-consistent state. Both languages carry equal authority;',
  85. '# after editing either side, bring the other along and re-record with:',
  86. `# pnpm run verify-translation-pairing --write ${paths.source}`,
  87. `${basename(paths.source)}: ${record.sourceHash}`,
  88. `${basename(paths.zh)}: ${record.zhHash}`,
  89. '',
  90. ].join('\n')
  91. }