diff --git a/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.i18n.yaml b/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.i18n.yaml index 4e2a2c5555..b9b6480ba8 100644 --- a/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.i18n.yaml +++ b/.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 diff --git a/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.md b/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.md index fab52cb70e..d8b792d5d1 100644 --- a/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.md +++ b/.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 diff --git a/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.zh.md b/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.zh.md index a86be2800a..ed49fccd97 100644 --- a/.agents/notes/implemented/process/2026-08-18-localized-bilingual-links.zh.md +++ b/.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 diff --git a/scripts/translation-links.spec.ts b/scripts/translation-links.spec.ts index 43b23dc100..8c7b5e8fa9 100644 --- a/scripts/translation-links.spec.ts +++ b/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']) diff --git a/scripts/translation-links.ts b/scripts/translation-links.ts index 8208307583..01d9023bb6 100644 --- a/scripts/translation-links.ts +++ b/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 -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, } }