✨ feat(vscode): 代码阅读中文直译里 Python 的 not x、TypeScript 的 !x 读成「x 不成立」(如果 root_dir.is_dir() 不成立),Python 走节点模板,TypeScript 用节点读法只换 !,去掉「非」的译法和它补空格的特例

This commit is contained in:
cruldra committed 2026-10-14 15:16:37 +11:00
1 parent e3c8c1b118
commit e1a5f4f012
9 files changed
+99 -35

No files matched your search

@@ -64,7 +64,6 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
'nonlocal': '非局部',
'del': '删除',
'assert': '断言',
'not': '非',
'and': '并且',
'or': '或者',
'is': '是',
@@ -95,5 +94,7 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
// `a if c else b` 的三个具名子节点依次是 a、c、b,语法没给它们起字段名
nodeTemplates: {
conditional_expression: ['如果', { namedChild: 1 }, '则', { namedChild: 0 }, '否则', { namedChild: 2 }],
// `not x` 读成「x 不成立」,照中文语序把条件放在前面;`not in`、`is not` 是单独的词,不走这里
not_operator: [{ field: 'argument' }, '不成立'],
},
})
@@ -1,6 +1,18 @@
import type { Node, Translator } from '../contract'
import type { NodeReader } from './zhTemplate'
import { createZhTemplateTranslator } from './zhTemplate'
/**
* `!x` 读成「x 不成立」,照中文语序把条件放在前面;别的一元运算(`-x`、`typeof x`)照常翻。
* 和节点模板一样,节点没整个落在要翻的这一段里也照常翻。
*/
const unaryExpression: NodeReader = (node, context) => {
const argument = node.childForFieldName('argument')
if (node.childForFieldName('operator')?.type !== '!' || !argument || !context.whole(node))
return undefined
return [...context.say(argument), { text: ' ', role: 'plain' }, { text: '不成立', role: 'keyword' }]
}
function commentText(_node: Node, text: string): string {
if (text.startsWith('//'))
return text.replace(/^\/{2,}\s?/, '')
@@ -80,8 +92,8 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
symbol: '符号',
},
isTypePosition: leaf => leaf.parent?.type === 'predefined_type',
operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '并且', '||': '或者', '!': '非' },
operatorParents: ['binary_expression', 'unary_expression'],
operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '并且', '||': '或者' },
operatorParents: ['binary_expression'],
emptyLiterals: { array: '空数组', object: '空对象' },
collections: { object: '对象', array: '数组' },
stringNodes: ['string', 'template_string', 'regex'],
@@ -89,4 +101,5 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
nodeTemplates: {
ternary_expression: ['如果', { field: 'condition' }, '则', { field: 'consequence' }, '否则', { field: 'alternative' }],
},
readers: { unary_expression: unaryExpression },
})
@@ -112,7 +112,7 @@ describe('typeScript 中文直译', () => {
it('spec 样例逐字一致', async () => {
const env = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
const issue = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
expect(text(env.get(25)?.segments)).toBe('如果 (非 folder)')
expect(text(env.get(25)?.segments)).toBe('如果 (folder 不成立)')
expect(text(env.get(26)?.segments)).toBe('返回 空数组')
expect(text(env.get(41)?.segments)).toBe('遍历 (常量 f 取自 files)')
expect(text(env.get(35)?.segments)).toBe('failed: { path: 字符串, reason: 字符串 }[]')
@@ -138,7 +138,7 @@ describe('typeScript 中文直译', () => {
'`.env` files are only picked up the next time the user clicks the button.',
].join('\n'))
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
expect(issue[15]).toBe('如果 (非 issue 或者 issue.prerequisite 全等 未定义)')
expect(issue[15]).toBe('如果 (issue 不成立 或者 issue.prerequisite 全等 未定义)')
})
it('每个被翻的词单独成段,着色取语法树词性', async () => {
@@ -165,7 +165,7 @@ describe('typeScript 中文直译', () => {
].join('\n')
const map = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))
expect(map[1]).toBe('o = { if: 1, return: i.number, string: x!.type, new: a.default }')
expect(map[2]).toBe('n = a 不全等 b 并且 非 c 或者 d 等于 e 不等于 f')
expect(map[2]).toBe('n = a 不全等 b 并且 c 不成立 或者 d 等于 e 不等于 f')
expect(map[3]).toBe('类型 S = 字符串 | 无')
// eslint-disable-next-line no-template-curly-in-string -- 模板字符串原样照抄进译文
expect(map[4]).toBe('s = 如果 类型为 v 全等 \'string\' 则 `${v}` 否则 \'number\'')
@@ -228,7 +228,7 @@ describe('typeScript 中文直译', () => {
1: '如果 err 属于 Error 则 err.message 否则 String(err)',
2: 'z = 如果 x 则 (如果 y 则 1 否则 2) 否则 3',
3: 'w = 如果 a 则 b 否则 如果 c 则 d 否则 e',
4: 'v = 如果 a 不全等 b 并且 非 c 则 类型为 d 否则 等待 e',
4: 'v = 如果 a 不全等 b 并且 c 不成立 则 类型为 d 否则 等待 e',
})
expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role === 'keyword')).toEqual([
{ text: '如果', role: 'keyword' },
@@ -237,6 +237,28 @@ describe('typeScript 中文直译', () => {
])
expectWordsApart(map)
})
it('取反 `!x` 读成「x 不成立」;别的一元运算、非空断言、不等号照旧', async () => {
const source = [
'if (!a) {}',
'if (!(a && b)) {}',
'x = -a + typeof b',
'y = c!.d !== e',
].join('\n')
const map = await translateAll(ts, 'typescript', typescriptZhTranslator, source)
expect(texts(map)).toEqual({
1: '如果 (a 不成立)',
2: '如果 ((a 并且 b) 不成立)',
3: 'x = -a + 类型为 b',
4: 'y = c!.d 不全等 e',
})
expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role !== 'plain')).toEqual([
{ text: '如果', role: 'keyword' },
{ text: 'a', role: 'variable', offset: source.indexOf('a') },
{ text: '不成立', role: 'keyword' },
])
expectWordsApart(map)
})
})
describe('python 中文直译', () => {
@@ -245,7 +267,7 @@ describe('python 中文直译', () => {
expect(texts(map)).toEqual({
1: 'Scan a workspace for .env files.',
12: '跳过备份文件',
14: '如果 非 found',
14: '如果 found 不成立',
15: '返回 空列表',
16: '否则如果 len(found) > MAX_DEPTH',
18: '否则',
@@ -341,7 +363,7 @@ describe('python 中文直译', () => {
1: '如果 _role 等于 "beat" 则 5680 否则 5679',
2: '如果 x 则 a 否则 如果 y 则 b 否则 c',
3: '如果 y 则 (如果 x 则 a 否则 b) 否则 c',
4: '如果 非 done 并且 ok 则 ⟦列表|遍历 items 的每个 k,仅当 k 不在 seen,取 k⟧ 否则 空',
4: '如果 done 不成立 并且 ok 则 ⟦列表|遍历 items 的每个 k,仅当 k 不在 seen,取 k⟧ 否则 空',
})
expect(flatSegments(valueOf(map.get(1), '初始值')).filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
{ text: '如果', role: 'keyword' },
@@ -350,6 +372,34 @@ describe('python 中文直译', () => {
{ text: '否则', role: 'keyword' },
])
})
it('`not x` 读成「x 不成立」:if 的条件、和 and 连着、括号里的、条件表达式里的;`not in`、`is not` 照旧', async () => {
const source = [
'def f(a, b, c, root_dir):',
' if not root_dir.is_dir():',
' return 1 if not b else 2',
' if not a and b:',
' pass',
' if not (a or b):',
' pass',
' if a not in b or a is not c:',
' pass',
].join('\n')
const map = await translateAll(py, 'python', pythonZhTranslator, source)
expect([2, 3, 4, 6, 8].map(line => text(map.get(line)?.segments))).toEqual([
'如果 root_dir.is_dir() 不成立',
'返回 如果 b 不成立 则 1 否则 2',
'如果 a 不成立 并且 b',
'如果 (a 或者 b) 不成立',
'如果 a 不在 b 或者 a 不是 c',
])
expect(flatSegments(inlineOf(map.get(4)?.segments ?? [])).filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
{ text: '如果', role: 'keyword' },
{ text: '不成立', role: 'keyword' },
{ text: '并且', role: 'operator' },
])
expect(linked(map.get(2)?.segments)).toEqual([['root_dir', source.indexOf('root_dir.')], ['is_dir', source.indexOf('is_dir')]])
})
})
describe('python 写在表达式里的遍历(推导式)', () => {
@@ -835,7 +885,7 @@ describe('属性表', () => {
'值: 对象 7 项',
' total | files.length',
' \'x-y\' | 等待 load()',
' [key] | 非 ok',
' [key] | ok 不成立',
' files | files',
' 展开 | rest',
' load | 异步 load(id: 字符串): Promise<无>',
@@ -897,7 +947,7 @@ describe('源码下标(跳到定义用)', () => {
const at = (word: string, from = 0): number => source.indexOf(word, from)
const map = await translateAll(py, 'python', pythonZhTranslator, source)
const ifLine = at('if')
expect(text(map.get(2)?.segments)).toBe('如果 非 user.active 并且 user 不是 空')
expect(text(map.get(2)?.segments)).toBe('如果 user.active 不成立 并且 user 不是 空')
expect(linked(map.get(2)?.segments)).toEqual([['user', at('user', ifLine)], ['active', at('active')], ['user', at('user is')]])
expect(linked(map.get(3)?.segments)).toEqual([['HTTPException', at('HTTPException')], ['status_code', at('status_code')]])
})
@@ -38,7 +38,7 @@ export interface ZhTable {
/** 内置类型:只在 isTypePosition 成立的叶子上按原文查。 */
builtinTypes: Record<string, string>
isTypePosition: (leaf: Node) => boolean
/** 翻成文字的运算符:只认父节点在 operatorParents 里的运算符节点(TS 的非空断言 `x!` 不算)。 */
/** 翻成文字的运算符:只认父节点在 `operatorParents` 里的运算符节点,同一个符号写在别处的照抄。 */
operators: Record<string, string>
operatorParents: string[]
/** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典 / 空对象。 */
@@ -60,7 +60,7 @@ export interface ZhTable {
*/
nodeTemplates: Record<string, TemplatePart[]>
/**
* 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`)。
* 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`,TypeScript 一元运算里只有 `!x` 换语序)。
* 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 undefined 就照常翻。读法里要翻子节点用 ReaderContext.say。
*/
readers?: Record<string, NodeReader>
@@ -903,8 +903,7 @@ function joinTokens(tokens: Token[]): Inline[] {
segments.push(token.group)
else
pushSegment(segments, token.text, token.role, token.offset)
// 「非」是个词,后面补一个空格,不和操作数粘在一起
gap = token.translated && token.raw === '!'
gap = false
})
return segments
}