✨ feat(vscode): 代码阅读中文直译里 Python 的 not x、TypeScript 的 !x 读成「x 不成立」(如果 root_dir.is_dir() 不成立),Python 走节点模板,TypeScript 用节点读法只换 !,去掉「非」的译法和它补空格的特例
This commit is contained in:
1 parent
e3c8c1b118
commit
e1a5f4f012
9 files changed
+99
-35
No files matched your search
@@ -349,7 +349,7 @@ describe('blockView', () => {
|
||||
' x | 整数 | ',
|
||||
'输出参数: 布尔',
|
||||
])
|
||||
expect(translationText(list[0].children[0])).toBe('返回 非 x')
|
||||
expect(translationText(list[0].children[0])).toBe('返回 x 不成立')
|
||||
})
|
||||
|
||||
it('跨几行写的调用:一行译文下面带摊开实参的表;calleeKind 交给翻译插件分新建还是调用', async () => {
|
||||
|
||||
@@ -600,7 +600,7 @@ export interface CommentBlock extends TextBlock { kind: 'comment' }
|
||||
export type TokenRole
|
||||
= | 'keyword' // 流程关键字:import from export if else for return try catch await …
|
||||
| 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null …
|
||||
| 'operator' // 被翻成文字的运算符:全等 并且 或者 非
|
||||
| 'operator' // 被翻成文字的运算符:全等 并且 或者
|
||||
| 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 …
|
||||
| 'constant' // 按习惯全大写、当常量用的名字(shell 的 PY_VERSION、HOME)
|
||||
| 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain'
|
||||
|
||||
@@ -305,7 +305,7 @@ describe('react 库规则:组件体一块一张卡片(luffy 的 PlanChecklis
|
||||
' 依赖项: collapsed(useCallback)',
|
||||
' variable 48',
|
||||
' 名称: next',
|
||||
' 初始值: 非 collapsed',
|
||||
' 初始值: collapsed 不成立',
|
||||
' statement 49: setCollapsed(next);',
|
||||
' try 50: 尝试',
|
||||
' statement 51: localStorage.setItem(PLAN_CHECKLIST_COLLAPSED_KEY, 如果 next 则 "1" 否则 "0");',
|
||||
@@ -323,7 +323,7 @@ describe('react 库规则:组件体一块一张卡片(luffy 的 PlanChecklis
|
||||
expect(render.badge).toEqual({ label: '渲染输出', color: { dark: '#89d185', light: '#388a34' }, icon: 'layout-template' })
|
||||
expect(lines(render).slice(0, 4)).toEqual([
|
||||
'JSX 结构大纲: ',
|
||||
' Collapsible ⟦样式 1 个类 · 取决于 CARD_SURFACE|⟧ open=非 collapsed onOpenChange=toggle',
|
||||
' Collapsible ⟦样式 1 个类 · 取决于 CARD_SURFACE|⟧ open=collapsed 不成立 onOpenChange=toggle',
|
||||
' CollapsibleTrigger ⟦样式 3 个类 · 取决于 FLUSH_ROW_INTERACTIVE|⟧ data-testid="plan-checklist-toggle"',
|
||||
' div ⟦样式 4 个类|⟧ data-testid="plan-progress"',
|
||||
])
|
||||
@@ -351,7 +351,7 @@ describe('react 库规则:luffy 的 QueuedBubble(原型「编辑器全貌」
|
||||
' variable 176',
|
||||
' 名称: el',
|
||||
' 初始值: textareaRef.current',
|
||||
' if 177: 如果 (非 el)',
|
||||
' if 177: 如果 (el 不成立)',
|
||||
' return 177: 返回;',
|
||||
' statement 178: el.focus();',
|
||||
' statement 179: el.setSelectionRange(el.value.length, el.value.length);',
|
||||
@@ -381,7 +381,7 @@ describe('react 库规则:luffy 的 QueuedBubble(原型「编辑器全貌」
|
||||
' textarea ⟦样式 6 个类 · 取决于 FIELD_TEXT|⟧ ref=textareaRef value=draft onChange=(e) => setDraft(e.target.value) aria-label="排队消息内容"',
|
||||
' div ⟦样式 3 个类|⟧',
|
||||
' Button variant="ghost" size="sm" onClick=onCancel "取消"',
|
||||
' Button size="sm" disabled=非 canSave onClick=save "保存"',
|
||||
' Button size="sm" disabled=canSave 不成立 onClick=save "保存"',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -600,10 +600,10 @@ describe('react 库规则:渲染大纲', () => {
|
||||
|
||||
it('luffy 的 DesktopSettingsDialog:先 return null 是如果卡片,最后返回的 JSX 是渲染输出;AppVaultSummary 的片段写「片段,不产生元素」', async () => {
|
||||
const dialog = await card(SETTINGS, 'DesktopSettingsDialog')
|
||||
expect(viewLines([childAt(dialog, 180)])).toEqual(['if 180: 如果 (非 section)', ' return 180: 返回 空;'])
|
||||
expect(viewLines([childAt(dialog, 180)])).toEqual(['if 180: 如果 (section 不成立)', ' return 180: 返回 空;'])
|
||||
expect(lines(childAt(dialog, 181)).slice(0, 2)).toEqual([
|
||||
'JSX 结构大纲: ',
|
||||
' Dialog open onOpenChange=(next) => 非 next 并且 closeSettings()',
|
||||
' Dialog open onOpenChange=(next) => next 不成立 并且 closeSettings()',
|
||||
])
|
||||
expect(lines(await render(readFixture('appVaultSummary.tsx.txt'), 'AppVaultSummary')).slice(0, 7)).toEqual([
|
||||
'JSX 结构大纲: ',
|
||||
@@ -649,7 +649,7 @@ describe('react 库规则:渲染大纲', () => {
|
||||
|
||||
it('luffy 的 DeleteAppDialog 的删除按钮:文字的三元写成是 / 否两支', async () => {
|
||||
const text = lines(await render(readFixture('deleteAppDialog.tsx.txt'), 'DeleteAppDialog'))
|
||||
const button = text.indexOf(' Button variant="destructive" onClick=() => remove.mutate() disabled=非 ready 或者 pending')
|
||||
const button = text.indexOf(' Button variant="destructive" onClick=() => remove.mutate() disabled=ready 不成立 或者 pending')
|
||||
expect(text.slice(button + 1, button + 4)).toEqual([
|
||||
' ⟦|如果 pending⟧',
|
||||
' ⟦|是⟧ "删除中…"',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export type BlockKind
|
||||
export type TokenRole
|
||||
= | 'keyword' // 流程关键字:import from export if else for return try catch await …
|
||||
| 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null …
|
||||
| 'operator' // 被翻成文字的运算符:全等 并且 或者 非
|
||||
| 'operator' // 被翻成文字的运算符:全等 并且 或者
|
||||
| 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 …
|
||||
| 'constant' // 按习惯全大写、当常量用的名字
|
||||
| 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain'
|
||||
|
||||
Reference in new issue
Block a user