✨ 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

+4 -2
View File
@@ -161,15 +161,17 @@ export interface CodeReaderApi {
其他规则:空数组字面量 → 空数组(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释去掉注释符号,原文照搬。关键字与前后内容之间保留源码里的空格(`!` 翻成「非」后补一个空格)。
关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:`raise_statement` 指父节点是 raise_statement,`for_statement > else_clause` 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。
TypeScript 关键字表(流程 = keyword,声明 = storage):
| 流程 | 声明 / 字面量 |
| --- | --- |
| import 导入、from 来自、as 作为、export 公共、default 默认、if 如果、else 否则、for 遍历、of 取自、in 在、while 当、do 执行、switch 分支、case 情况、break 跳出、continue 继续、return 返回、throw 抛出、try 尝试、catch 捕获、finally 最终、await 等待、yield 产出 | const 常量、let 变量、var 变量、function 函数、async 异步、class 类、extends 继承、implements 实现、interface 接口、type 类型、enum 枚举、new 新建、instanceof 属于、typeof 类型为、keyof 键名、readonly 只读、public 公共、private 私有、protected 受保护、static 静态、abstract 抽象、delete 删除、true 真、false 假、null 空、undefined 未定义 |
| import 导入、from 来自、as(导入 / 导出别名)作为 / (类型断言 `x as T`、`as const`)断言为、export 公共、default(`export default`)默认 / (`switch` 里的 `default:`)默认情况、if 如果、else 否则、for 遍历、of 取自、in(二元运算 `'k' in obj`)在 / (`for…in` 头部)取自、while 当、do 执行、switch 分支、case 情况、break 跳出、continue 继续、return 返回、throw 抛出、try 尝试、catch 捕获、finally 最终、await 等待、yield 产出 | const 常量、let 变量、var 变量、function 函数、async 异步、class 类、extends(类 / 接口继承)继承 / (泛型约束 `T extends X`、`infer U extends X`)限定为 / (条件类型 `A extends B ? … : …`)符合、implements 实现、interface 接口、type 类型、enum 枚举、new 新建、instanceof 属于、typeof 类型为、keyof 键名、readonly 只读、public 公共、private 私有、protected 受保护、static 静态、abstract 抽象、delete 删除、true 真、false 假、null 空、undefined 未定义 |
TypeScript 内置类型:string 字符串、number 数值、boolean 布尔、any 任意、unknown 未知、never 永不、void 无、object 对象、bigint 大整数、symbol 符号。`Promise` 等库类型不翻。
Python 关键字:import 导入、from 从、as 作为、def 函数、async 异步、await 等待、class 类、return 返回、if 如果、elif 否则如果、else 否则、for 遍历、in(for 头部与推导式里)取自 / (比较里)在、while 当、try 尝试、except 捕获、finally 最终、raise 抛出、with 使用、yield 产出、lambda 匿名函数、pass 跳过、break 跳出、continue 继续、global 全局、nonlocal 非局部、del 删除、assert 断言、not 非、and 与、or 或、is 是、None 空、True 真、False 假、match 匹配、case 情况。
Python 关键字:import 导入、from(导入)从 / (`raise … from`)源自 / (`yield from`)自、as(导入别名、`with … as`、`except … as`)作为、def 函数、async 异步、await 等待、class 类、return 返回、if 如果、elif 否则如果、else(`if` 后、条件表达式里)否则 / (`for` / `while` 后,循环没被 break 才执行)未中断时 / (`try` 后,没出错才执行)未出错时、for 遍历、in(比较里)在 / (for 头部与推导式里)取自、while 当、try 尝试、except 捕获、finally 最终、raise 抛出、with 使用、yield 产出、lambda 匿名函数、pass 跳过、break 跳出、continue 继续、global 全局、nonlocal 非局部、del 删除、assert 断言、not 非、and 与、or 或、is 是、not in 不在、is not 不是、None 空、True 真、False 假、match 匹配、case 情况。
Python 内置类型(仅注解里):str 字符串、int 整数、float 浮点数、bool 布尔、list 列表、dict 字典、set 集合、tuple 元组、bytes 字节串。
+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])