refactor(i18n): remove directory index inference

This commit is contained in:
pku-xht 2026-08-19 02:52:15 +08:00
parent 8a334c4d49
commit f5eb06915a
5 changed files with 18 additions and 59 deletions

View file

@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-18-localized-bilingual-links.md
2026-08-18-localized-bilingual-links.md: fab52cb70e611c25842883513bb89e1e1861a935
2026-08-18-localized-bilingual-links.zh.md: a86be2800adaa7113b8da78ac4420ae48fde1663
2026-08-18-localized-bilingual-links.md: d8b792d5d1e9c23a1e6389328ed02cf2fb337a0e
2026-08-18-localized-bilingual-links.zh.md: ed49fccd975c6681831724d3577104a8ae7221cf

View file

@ -22,7 +22,7 @@ Existing active bilingual sources use the locale-correct target. A Chinese targe
## Verification
Pairing tests cover English and Chinese locale selection, out-of-scope targets with siblings, in-scope targets missing a counterpart, switcher exclusion, exact query/fragment retention, directory-index resolution, definitions, rewrites, and diagnostics. Documentation-site tests also pin the audited basic, framework, and practice entry links to explicit index pages in both locales. Merge-driver, translation-brief, and Cordis generator tests cover their respective consumers. Corpus checks require zero wrong-locale links, resolvable fragments, fresh generated regions, current pair records, and a successful documentation-site build.
Pairing tests cover English and Chinese locale selection, out-of-scope targets with siblings, in-scope targets missing a counterpart, switcher exclusion, exact query/fragment retention, non-inference of directory targets, definitions, rewrites, and diagnostics. Documentation-site tests also pin the audited basic, framework, and practice entry links to explicit index pages in both locales. Merge-driver, translation-brief, and Cordis generator tests cover their respective consumers. Corpus checks require zero wrong-locale links, resolvable fragments, fresh generated regions, current pair records, and a successful documentation-site build.
## Alternatives considered

View file

@ -22,7 +22,7 @@ Cordis subsystem 区块生成器先渲染同一个 catalog model,再为中文
## Verification
配对测试覆盖中英文 locale 选择、存在兄弟文件的范围外目标、范围内缺少对侧的目标、语言切换行排除、query/fragment 原样保留、目录索引解析、定义式链接、重写与诊断。文档网站测试还会固定中英文两侧已审计的 basic、framework 与 practice 入口,要求它们指向具体索引页。合并驱动、翻译简报与 Cordis 生成器测试分别覆盖各自消费路径。全语料检查要求 locale 错误链接为零、fragment 可解析、生成区块新鲜、配对记录为当前内容,并要求文档网站成功构建。
配对测试覆盖中英文 locale 选择、存在兄弟文件的范围外目标、范围内缺少对侧的目标、语言切换行排除、query/fragment 原样保留、不从目录目标推断索引页、定义式链接、重写与诊断。文档网站测试还会固定中英文两侧已审计的 basic、framework 与 practice 入口,要求它们指向具体索引页。合并驱动、翻译简报与 Cordis 生成器测试分别覆盖各自消费路径。全语料检查要求 locale 错误链接为零、fragment 可解析、生成区块新鲜、配对记录为当前内容,并要求文档网站成功构建。
## Alternatives considered

View file

@ -96,12 +96,12 @@ describe('translation link locale validation', () => {
})
})
it('retains an English directory-index alias', () => {
it('does not infer an index page from a directory target', () => {
const root = fixture()
expect(translationLinkLocaleViolations(
'[Section](section/)\n',
linkContext(root, 'docs/guide.md'),
)).toEqual([])
const input = '[Section](section/)\n'
expect(translationLinkLocaleViolations(input, linkContext(root, 'docs/guide.zh.md'))).toEqual([])
expect(rewriteTranslationLinkLocales(input, linkContext(root, 'docs/guide.zh.md')))
.toEqual({ content: input, rewritten: 0 })
})
it('exempts the language switcher target explicitly', () => {
@ -133,21 +133,6 @@ describe('translation link locale validation', () => {
).content).toBe('# 指南\n\n[English](guide.md) | 中文\n\n[正文](guide.zh.md)\n')
})
it('resolves a directory alias to its paired index page', () => {
const root = fixture()
expect(translationLinkLocaleViolations(
'[章节](section/)\n',
linkContext(root, 'docs/guide.zh.md'),
)[0]).toMatchObject({ expectedUrl: 'section/index.zh.md' })
expect(normalizeTranslationMarkdownLinks(
'[Section](section/)\n',
linkContext(root, 'docs/guide.md'),
)).toBe(normalizeTranslationMarkdownLinks(
'[Section](section/index.zh.md)\n',
linkContext(root, 'docs/guide.zh.md'),
))
})
it('uses the selected content plane for target existence without deriving scope from siblings', () => {
const root = fixture()
const staged = new Set(['docs/reference.md', 'docs/reference.zh.md'])

View file

@ -49,7 +49,6 @@ interface ResolvedTranslationLink {
suffix: string
expectedPath: string
expectedUrl: string
kind: ResolutionKind
locale: 'en' | 'zh'
}
@ -60,7 +59,6 @@ interface Replacement {
}
type LinkNode = Extract<Nodes, { type: 'link' | 'definition' }>
type ResolutionKind = 'exact' | 'directory-index'
/** Offset of the one top-level switcher link immediately following the H1. */
export function languageSwitcherLinkOffset(
@ -129,19 +127,11 @@ function repositoryRelativePath(path: string): string | undefined {
function resolveRepositoryTarget(
rawPath: string,
context: TranslationLinkContext,
): { path: string; kind: ResolutionKind } | undefined {
): string | 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
return repositoryFileExists(context, exact) ? exact : undefined
}
function translationPairTarget(targetPath: string, context: TranslationLinkContext): TranslationPairTarget | undefined {
@ -153,29 +143,15 @@ function translationPairTarget(targetPath: string, context: TranslationLinkConte
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 (locale === 'zh' && rawPath.endsWith('.md') && !rawPath.endsWith('.zh.md')) {
return rawPath.replace(/\.md$/, '.zh.md')
}
if (kind === 'directory-index' && locale === 'zh') {
return `${rawPath}${rawPath.endsWith('/') ? '' : '/'}index.zh.md`
}
return fallbackRelativePath(context, targetPath, rawPath)
if (locale === 'en' && rawPath.endsWith('.zh.md')) return rawPath.replace(/\.zh\.md$/, '.md')
return rawPath
}
function resolveTranslationLink(
@ -187,9 +163,8 @@ function resolveTranslationLink(
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 targetPath = resolveRepositoryTarget(path, context)
if (targetPath === undefined) return undefined
const pair = translationPairTarget(targetPath, context)
if (pair === undefined) return undefined
const locale = context.sourcePath.endsWith('.zh.md') ? 'zh' : 'en'
@ -199,8 +174,7 @@ function resolveTranslationLink(
targetPath,
suffix: authored.suffix,
expectedPath,
expectedUrl: `${expectedLocalePath(authored.path, resolved.kind, locale, context, expectedPath)}${authored.suffix}`,
kind: resolved.kind,
expectedUrl: `${expectedLocalePath(authored.path, locale)}${authored.suffix}`,
locale,
}
}