Kaynağa Gözat

refactor(i18n): remove directory index inference

pku-xht 3 hafta önce
ebeveyn
işleme
f5eb06915a

+ 2 - 2
.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.i18n.yaml

@@ -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

+ 1 - 1
.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.md

@@ -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
 

+ 1 - 1
.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.zh.md

@@ -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
 

+ 5 - 20
scripts/translation-links.spec.ts

@@ -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'])

+ 9 - 35
scripts/translation-links.ts

@@ -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 (kind === 'directory-index' && locale === 'zh') {
-    return `${rawPath}${rawPath.endsWith('/') ? '' : '/'}index.zh.md`
+  if (locale === 'zh' && rawPath.endsWith('.md') && !rawPath.endsWith('.zh.md')) {
+    return rawPath.replace(/\.md$/, '.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,
   }
 }