deepseek-harness/scripts/translation-links.ts

277 lines
9.4 KiB
TypeScript
Raw Normal View History

/** Locale-aware resolution and byte-preserving rewrites for bilingual Markdown links. */
import { existsSync, statSync } from 'node:fs'
import { posix, resolve } from 'node:path'
import type { Nodes } from 'mdast'
2026-08-18 19:37:53 +08:00
import {
isExternalOrAbsoluteMarkdownUrl,
markdownDestination,
parseMarkdown,
splitMarkdownUrlTarget,
visitMarkdown,
type MarkdownDestination,
} from './markdown.ts'
/** Repository and source document used to resolve one relative link. */
export interface TranslationLinkContext {
/** Absolute repository root. */
repoRoot: string
/** Repository-relative Markdown source path. */
sourcePath: string
/** Selected content plane; defaults to regular files in the working tree. */
repositoryFileExists?: (repoPath: string) => boolean
}
/** One relative document link whose target uses the wrong locale sibling. */
export interface TranslationLinkLocaleViolation {
sourcePath: string
line: number
url: string
expectedUrl: string
}
/** Result of rewriting wrong-locale relative document links. */
export interface TranslationLinkRewriteResult {
content: string
rewritten: number
}
interface TranslationPairTarget {
source: string
zh: string
}
interface ResolvedTranslationLink {
pair: TranslationPairTarget
targetPath: string
suffix: string
expectedPath: string
expectedUrl: string
kind: ResolutionKind
locale: 'en' | 'zh'
}
interface Replacement {
start: number
end: number
value: string
}
type LinkNode = Extract<Nodes, { type: 'link' | 'definition' }>
type ResolutionKind = 'exact' | 'directory-index'
function decodePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
function worktreeFileExists(repoRoot: string, repoPath: string): boolean {
try {
const path = resolve(repoRoot, repoPath)
return existsSync(path) && statSync(path).isFile()
} catch {
return false
}
}
function repositoryFileExists(context: TranslationLinkContext, repoPath: string): boolean {
return context.repositoryFileExists?.(repoPath) ?? worktreeFileExists(context.repoRoot, repoPath)
}
function repositoryRelativePath(path: string): string | undefined {
const normalized = posix.normalize(path)
if (normalized === '' || normalized === '.' || normalized === '..' || normalized.startsWith('../') || posix.isAbsolute(normalized)) {
return undefined
}
return normalized
}
function resolveRepositoryTarget(
rawPath: string,
context: TranslationLinkContext,
): { path: string; kind: ResolutionKind } | undefined {
const decoded = decodePath(rawPath)
const exact = repositoryRelativePath(posix.join(posix.dirname(context.sourcePath), decoded))
if (exact === undefined) return undefined
if (repositoryFileExists(context, exact)) return { path: exact, kind: 'exact' }
const index = repositoryRelativePath(posix.join(exact, 'index.md'))
if (decoded.endsWith('/') && index !== undefined && repositoryFileExists(context, index)) {
return { path: index, kind: 'directory-index' }
}
if (posix.extname(decoded) === '' && index !== undefined && repositoryFileExists(context, index)) {
return { path: index, kind: 'directory-index' }
}
return undefined
}
function translationPairTarget(targetPath: string, context: TranslationLinkContext): TranslationPairTarget | undefined {
const source = targetPath.endsWith('.zh.md')
? targetPath.replace(/\.zh\.md$/, '.md')
: targetPath.endsWith('.md') ? targetPath : undefined
if (source === undefined) return undefined
const zh = source.replace(/\.md$/, '.zh.md')
if (!repositoryFileExists(context, source) || !repositoryFileExists(context, zh)) return undefined
return { source, zh }
}
function fallbackRelativePath(context: TranslationLinkContext, targetPath: string, rawPath: string): string {
const target = posix.relative(posix.dirname(context.sourcePath), targetPath)
const encoded = encodeURI(target)
return rawPath.startsWith('./') && !encoded.startsWith('.') ? `./${encoded}` : encoded
}
function expectedLocalePath(
rawPath: string,
kind: ResolutionKind,
locale: 'en' | 'zh',
context: TranslationLinkContext,
targetPath: string,
): string {
if (kind === 'exact') {
if (locale === 'zh' && rawPath.endsWith('.md') && !rawPath.endsWith('.zh.md')) {
return rawPath.replace(/\.md$/, '.zh.md')
}
if (locale === 'en' && rawPath.endsWith('.zh.md')) return rawPath.replace(/\.zh\.md$/, '.md')
}
if (kind === 'directory-index' && locale === 'zh') {
return `${rawPath}${rawPath.endsWith('/') ? '' : '/'}index.zh.md`
}
return fallbackRelativePath(context, targetPath, rawPath)
}
function resolveTranslationLink(
url: string,
context: TranslationLinkContext,
authoredUrl: string = url,
): ResolvedTranslationLink | undefined {
2026-08-18 19:37:53 +08:00
if (isExternalOrAbsoluteMarkdownUrl(url)) return undefined
const { path } = splitMarkdownUrlTarget(url)
const authored = splitMarkdownUrlTarget(authoredUrl)
if (path === '') return undefined
const resolved = resolveRepositoryTarget(path, context)
if (resolved === undefined) return undefined
const targetPath = resolved.path
const pair = translationPairTarget(targetPath, context)
if (pair === undefined) return undefined
const locale = context.sourcePath.endsWith('.zh.md') ? 'zh' : 'en'
const expectedPath = locale === 'zh' ? pair.zh : pair.source
return {
pair,
targetPath,
suffix: authored.suffix,
expectedPath,
expectedUrl: `${expectedLocalePath(authored.path, resolved.kind, locale, context, expectedPath)}${authored.suffix}`,
kind: resolved.kind,
locale,
}
}
function hasExpectedLocale(resolved: ResolvedTranslationLink): boolean {
return resolved.targetPath === resolved.expectedPath
}
2026-08-18 19:37:53 +08:00
function replacementFor(destination: MarkdownDestination, value: string): Replacement {
return { start: destination.start, end: destination.end, value }
}
function applyReplacements(markdown: string, replacements: Replacement[]): string {
let output = markdown
for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
output = output.slice(0, replacement.start) + replacement.value + output.slice(replacement.end)
}
return output
}
function visitDocumentLinkNodes(markdown: string, visitor: (node: LinkNode) => void): void {
const tree = parseMarkdown(markdown)
const linkDefinitions = new Set<string>()
visitMarkdown(tree, (node) => {
if (node.type === 'linkReference') linkDefinitions.add(node.identifier)
})
visitMarkdown(tree, (node) => {
if (node.type === 'link' || (node.type === 'definition' && linkDefinitions.has(node.identifier))) {
visitor(node)
}
})
}
2026-08-18 19:37:53 +08:00
function visitResolvedDocumentLinks(
markdown: string,
context: TranslationLinkContext,
skipTargets: readonly string[],
visitor: (node: LinkNode, destination: MarkdownDestination, resolved: ResolvedTranslationLink) => void,
): void {
const skipped = new Set(skipTargets)
visitDocumentLinkNodes(markdown, (node) => {
if (skipped.has(node.url)) return
const destination = markdownDestination(markdown, node)
const resolved = resolveTranslationLink(node.url, context, destination.url)
if (resolved !== undefined) visitor(node, destination, resolved)
})
}
/** Return one violation per wrong-locale link or link definition. */
export function translationLinkLocaleViolations(
markdown: string,
context: TranslationLinkContext,
skipTargets: readonly string[] = [],
): TranslationLinkLocaleViolation[] {
const violations: TranslationLinkLocaleViolation[] = []
2026-08-18 19:37:53 +08:00
visitResolvedDocumentLinks(markdown, context, skipTargets, (node, destination, resolved) => {
if (hasExpectedLocale(resolved)) return
violations.push({
sourcePath: context.sourcePath,
line: node.position?.start.line ?? 0,
2026-08-18 19:37:53 +08:00
url: destination.url,
expectedUrl: resolved.expectedUrl,
})
})
return violations
}
/** Rewrite wrong-locale document links without reserializing surrounding Markdown. */
export function rewriteTranslationLinkLocales(
markdown: string,
context: TranslationLinkContext,
skipTargets: readonly string[] = [],
): TranslationLinkRewriteResult {
const replacements: Replacement[] = []
2026-08-18 19:37:53 +08:00
visitResolvedDocumentLinks(markdown, context, skipTargets, (_node, destination, resolved) => {
if (hasExpectedLocale(resolved)) return
replacements.push(replacementFor(destination, resolved.expectedUrl))
})
return { content: applyReplacements(markdown, replacements), rewritten: replacements.length }
}
/** Normalize only paired-document locale paths while retaining every other byte and URL suffix. */
export function normalizeTranslationMarkdownLinks(
markdown: string,
context: TranslationLinkContext,
skipTargets: readonly string[] = [],
): string {
const replacements: Replacement[] = []
2026-08-18 19:37:53 +08:00
visitResolvedDocumentLinks(markdown, context, skipTargets, (_node, destination, resolved) => {
replacements.push(replacementFor(
2026-08-18 19:37:53 +08:00
destination,
`dsh-translation-target:${resolved.pair.source}${resolved.suffix}`,
))
})
return applyReplacements(markdown, replacements)
}
/** Semantic target of one authored inline link or referenced definition. */
export function semanticTranslationLinkNodeTarget(
node: LinkNode,
markdown: string,
context: TranslationLinkContext,
): string {
2026-08-18 19:37:53 +08:00
const destination = markdownDestination(markdown, node)
const resolved = resolveTranslationLink(node.url, context, destination.url)
return resolved === undefined
2026-08-18 19:37:53 +08:00
? destination.url
: `dsh-translation-target:${resolved.pair.source}${resolved.suffix}`
}