✨ feat(vscode): 代码阅读关键字按语境翻译

This commit is contained in:
cruldra committed 2026-09-25 01:58:07 +08:00
1 parent 72d33032f0
commit d134837c17
5 files changed
+155 -19

No files matched your search

+11 -3
View File
@@ -20,7 +20,7 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
tokenRole: pythonTokenRole,
keywords: {
'import': '导入',
'from': '从',
'from': { zh: '从', byContext: { raise_statement: '源自', yield: '自' } },
'as': '作为',
'def': '函数',
'async': '异步',
@@ -29,9 +29,17 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
'return': '返回',
'if': '如果',
'elif': '否则如果',
'else': '否则',
// 循环的 else 只在没被 break 打断时执行,try 的 else 只在没抛异常时执行
'else': {
zh: '否则',
byContext: {
'for_statement > else_clause': '未中断时',
'while_statement > else_clause': '未中断时',
'try_statement > else_clause': '未出错时',
},
},
'for': '遍历',
'in': { zh: '在', byParent: { for_statement: '取自', for_in_clause: '取自' } },
'in': { zh: '在', byContext: { for_statement: '取自', for_in_clause: '取自' } },
'while': '当',
'try': '尝试',
'except': '捕获',
@@ -23,14 +23,14 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
// 流程
import: '导入',
from: '来自',
as: '作为',
as: { zh: '作为', byContext: { as_expression: '断言为' } },
export: '公共',
default: '默认',
default: { zh: '默认', byContext: { switch_default: '默认情况' } },
if: '如果',
else: '否则',
for: '遍历',
of: '取自',
in: '在',
in: { zh: '在', byContext: { for_in_statement: '取自' } },
while: '当',
do: '执行',
switch: '分支',
@@ -51,7 +51,8 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
function: '函数',
async: '异步',
class: '类',
extends: '继承',
// infer U extends X 同样是给类型参数加约束
extends: { zh: '继承', byContext: { constraint: '限定为', infer_type: '限定为', conditional_type: '符合' } },
implements: '实现',
interface: '接口',
type: '类型',
@@ -5,6 +5,7 @@ import { createTypeScriptAdapter } from '../languages/typescript'
import { readFixture, walk, WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from './pythonZh'
import { typescriptZhTranslator } from './typescriptZh'
import { createZhTemplateTranslator } from './zhTemplate'
const ts = createTypeScriptAdapter(WASM_DIR)
const py = createPythonAdapter(WASM_DIR)
@@ -124,6 +125,39 @@ describe('typeScript 中文直译', () => {
].join('\n')
expect(texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))[1]).toBe('call( first, 空数组 )')
})
it('as / in / extends / default 按所在语法节点换译法', async () => {
const source = [
'import * as path from \'node:path\'',
'export { run as start } from \'./run\'',
'const v = x as string',
'const k = [\'a\'] as const',
'for (const key in obj) {}',
'const has = \'k\' in obj',
'class A extends B implements C {}',
'interface I extends J, K {}',
'function f<T extends X>(t: T) {}',
'type R<T> = T extends string ? 1 : 2',
'export default function g() {}',
'switch (x) {',
' default:',
' break',
'}',
].join('\n')
const map = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))
expect(map[1]).toBe('导入 * 作为 path 来自 \'node:path\'')
expect(map[2]).toBe('公共 { run 作为 start } 来自 \'./run\'')
expect(map[3]).toBe('常量 v = x 断言为 字符串')
expect(map[4]).toBe('常量 k = [\'a\'] 断言为 常量')
expect(map[5]).toBe('遍历 (常量 key 取自 obj)')
expect(map[6]).toBe('常量 has = \'k\' 在 obj')
expect(map[7]).toBe('类 A 继承 B 实现 C')
expect(map[8]).toBe('接口 I 继承 J, K')
expect(map[9]).toBe('函数 f<T 限定为 X>(t: T)')
expect(map[10]).toBe('类型 R<T> = T 符合 字符串 ? 1 : 2')
expect(map[11]).toBe('公共 默认 函数 g()')
expect(map[13]).toBe('默认情况')
})
})
describe('python 中文直译', () => {
@@ -152,7 +186,7 @@ describe('python 中文直译', () => {
24: '使用 open(os.path.join(root, found[0])) 作为 handle',
25: 'handle.read()',
26: '捕获 OSError 作为 err',
27: '抛出 RuntimeError(str(err)) 从 err',
27: '抛出 RuntimeError(str(err)) 源自 err',
28: '最终',
29: 'skipped.clear()',
30: '返回 found',
@@ -177,14 +211,81 @@ describe('python 中文直译', () => {
expect(map[2]).toBe('返回 [str(k) 遍历 k 取自 items 如果 k 不在 list]')
})
it('from 在句首译成「从」,yield from 同一个词', async () => {
it('from:导入里「从」,raise … from 里「源自」,yield from 里「自」', async () => {
const source = [
'from typing import Annotated',
'def walk(nodes):',
' yield from nodes',
' raise RuntimeError(str(err)) from err',
].join('\n')
const map = texts(await translateAll(py, 'python', pythonZhTranslator, source))
expect(map[1]).toBe('从 typing 导入 Annotated')
expect(map[3]).toBe('产出 从 nodes')
expect(map[3]).toBe('产出 自 nodes')
expect(map[4]).toBe('抛出 RuntimeError(str(err)) 源自 err')
})
it('else:if 后「否则」,for / while 后「未中断时」,try 后「未出错时」', async () => {
const source = [
'for k in items:',
' if k:',
' break',
' else:',
' pass',
'else:',
' pass',
'while busy:',
' pass',
'else:',
' pass',
'try:',
' pass',
'except OSError:',
' pass',
'else:',
' pass',
].join('\n')
const map = texts(await translateAll(py, 'python', pythonZhTranslator, source))
expect(map[4]).toBe('否则')
expect(map[6]).toBe('未中断时')
expect(map[10]).toBe('未中断时')
expect(map[16]).toBe('未出错时')
})
})
describe('中文模板引擎', () => {
it('关键字按父节点类型选译法,更长的节点类型链优先,都不匹配用默认', async () => {
const translator = createZhTemplateTranslator({
id: 'test.zh-template',
label: '测试',
languageIds: ['python'],
tokenRole: () => 'keyword',
keywords: {
in: { zh: '甲', byContext: { for_statement: '乙' } },
else: { zh: '丙', byContext: { 'else_clause': '丁', 'for_statement > else_clause': '戊' } },
},
literals: {},
builtinTypes: {},
isTypePosition: () => false,
operators: {},
operatorParents: [],
emptyLiterals: {},
stringNodes: [],
commentText: node => node.text,
})
const source = [
'for k in items:',
' if k in seen:',
' pass',
' else:',
' x = a if c else b',
'else:',
' pass',
].join('\n')
const map = texts(await translateAll(py, 'python', translator, source))
expect(map[1]).toBe('for k 乙 items')
expect(map[2]).toBe('if k 甲 seen')
expect(map[4]).toBe('丁')
expect(map[5]).toBe('x = a if c 丙 b')
expect(map[6]).toBe('戊')
})
})
@@ -1,7 +1,16 @@
import type { CodeBlock, Node, Segment, SourceSpan, TokenRole, Translator } from '../contract'
/** 按父节点类型区分译法的关键字,例如 Python 的 in:for 头部里「取自」,比较里「在」。 */
export interface ContextualWord { zh: string, byParent: Record<string, string> }
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
export interface ContextualWord {
/** 没有匹配的语境时用的译法。 */
zh: string
/**
* 语境 → 译法。语境是关键字节点往上的节点类型链,外层在前、用 ` > ` 连接:
* `raise_statement` 指父节点,`for_statement > else_clause` 指父节点 else_clause 且祖父节点 for_statement。
* 同时匹配多条时取最长的那条。
*/
byContext: Record<string, string>
}
/** 一种语言的中文直译表。引擎只替换表里的三类东西,其余原样照抄。 */
export interface ZhTable {
@@ -10,7 +19,7 @@ export interface ZhTable {
languageIds: string[]
/** 未翻译的词按源码着色的词性上色;关键字译文的词性也取它。 */
tokenRole: (leaf: Node) => TokenRole
/** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点)。 */
/** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点);随所在语法节点换译法的写成 ContextualWord。 */
keywords: Record<string, string | ContextualWord>
/** 字面量关键字:具名的叶子节点(true / None …),按节点类型查。 */
literals: Record<string, string>
@@ -38,6 +47,7 @@ interface Token {
raw: string
}
const CONTEXT_SEPARATOR = ' > '
const CLOSING = new Set([')', ']', '}'])
const HEADER_OPENERS = new Set(['{', ':'])
@@ -70,6 +80,22 @@ function oneLine(text: string): string {
export function createZhTemplateTranslator(table: ZhTable): Translator {
const operatorParents = new Set(table.operatorParents)
const stringNodes = new Set(table.stringNodes)
const contextDepth = Math.max(0, ...Object.values(table.keywords).flatMap(word =>
typeof word === 'string' ? [] : Object.keys(word.byContext).map(context => context.split(CONTEXT_SEPARATOR).length)))
/** 从父节点往上逐层拼出语境去查,更长(更具体)的匹配覆盖更短的。 */
function keywordText(word: string | ContextualWord, node: Node): string {
if (typeof word === 'string')
return word
let text = word.zh
let context = ''
let ancestor = node.parent
for (let level = 0; ancestor && level < contextDepth; level++, ancestor = ancestor.parent) {
context = context ? `${ancestor.type}${CONTEXT_SEPARATOR}${context}` : ancestor.type
text = word.byContext[context] ?? text
}
return text
}
/** 能整个换成中文的节点 → 译文和词性。 */
function translateNode(node: Node): { text: string, role: TokenRole } | undefined {
@@ -83,10 +109,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
if (parent && operatorParents.has(parent.type) && table.operators[node.type])
return { text: table.operators[node.type], role: 'operator' }
const word = table.keywords[node.type]
if (word !== undefined) {
const text = typeof word === 'string' ? word : (parent && word.byParent[parent.type]) || word.zh
return { text, role: table.tokenRole(firstLeaf(node)) }
}
if (word !== undefined)
return { text: keywordText(word, node), role: table.tokenRole(firstLeaf(node)) }
return undefined
}
if (node.childCount === 0 && table.literals[node.type])