✨ feat(vscode): 条件表达式按「如果…则…否则…」翻译
This commit is contained in:
1 parent
3a184eed9b
commit
6897954644
5 files changed
+212
-27
No files matched your search
@@ -153,7 +153,7 @@ export interface CodeReaderApi {
|
||||
|
||||
## 中文模板直译
|
||||
|
||||
结构照抄源码,只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:
|
||||
结构照抄源码(按节点模板重排的语法结构除外,见下文),只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:
|
||||
|
||||
1. 关键字:只认语法树里的关键字节点(匿名节点),所以属性名 `i.number`、对象键不会被误翻。
|
||||
2. 内置类型:只在类型位置翻(TS 的 `predefined_type`、Python 注解里的内置类型名)。
|
||||
@@ -163,6 +163,13 @@ export interface CodeReaderApi {
|
||||
|
||||
关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:`raise_statement` 指父节点是 raise_statement,`for_statement > else_clause` 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。
|
||||
|
||||
节点模板:照抄源码结构读不通的语法结构,按语法节点类型给一个模板,把译文重排成中文语序。模板是一串项,每项要么是插入的译词,要么是子节点槽位:槽位按 tree-sitter 字段名引用子节点;语法没给字段名的,按具名子节点序号(0 起,注释不占序号)引用。命中模板的节点只输出模板里的项,节点自己的关键字和标点(`if` / `else`、`?` / `:`)不再输出;插入的译词一律按关键字(keyword)着色,模板各项之间空一格;槽位里的子节点照常递归翻译,里面的关键字、运算符、嵌套的同类结构照样处理。两种情况不重排、照常翻:模板引用的子节点不存在;节点没整个落在要翻的这一段里。目前的模板:
|
||||
|
||||
| 语言 | 节点类型 | 模板 |
|
||||
| --- | --- | --- |
|
||||
| TypeScript | `ternary_expression`(`c ? a : b`) | 如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉 |
|
||||
| Python | `conditional_expression`(`a if c else b`,没有字段名,具名子节点依次是 a、c、b) | 如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉 |
|
||||
|
||||
TypeScript 关键字表(流程 = keyword,声明 = storage):
|
||||
|
||||
| 流程 | 声明 / 字面量 |
|
||||
@@ -171,11 +178,11 @@ TypeScript 关键字表(流程 = keyword,声明 = storage):
|
||||
|
||||
TypeScript 内置类型:string 字符串、number 数值、boolean 布尔、any 任意、unknown 未知、never 永不、void 无、object 对象、bigint 大整数、symbol 符号。`Promise` 等库类型不翻。
|
||||
|
||||
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 关键字: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 字节串。
|
||||
|
||||
样例(必须逐字一致,来自原型 `test.js`):
|
||||
样例(必须逐字一致;条件表达式两行以外来自原型 `test.js`):
|
||||
|
||||
| 源码 | 译文 |
|
||||
| --- | --- |
|
||||
@@ -189,6 +196,8 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮
|
||||
| `const issue = issues.find(i => i.number === issueNumber)` | `常量 issue = issues.find(i => i.number 全等 issueNumber)` |
|
||||
| `return {⏎ locked: true,⏎ … }`(issueLock.ts 20–24 行) | `返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column }` |
|
||||
| `def find_env_files(root: str) -> list[str]:` | `函数 find_env_files(root: 字符串) -> 列表[字符串]` |
|
||||
| `err instanceof Error ? err.message : String(err)` | `如果 err 属于 Error 则 err.message 否则 String(err)` |
|
||||
| `_default_port = 5680 if _role == "beat" else 5679` | `_default_port = 如果 _role 等于 "beat" 则 5680 否则 5679` |
|
||||
|
||||
## 预览数据与消息
|
||||
|
||||
@@ -255,7 +264,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
|
||||
## 测试
|
||||
|
||||
- 语言插件:`envLock.ts`、`issueLock.ts` 的代码块树(种类、header 行号、层级)与原型 `CODE_TREE` 一致;Python 样例覆盖 import / def / docstring / if-elif-else / for / try-except / with / 推导式。
|
||||
- 翻译:上面样例表逐字一致;属性名不被翻;每个被翻的词单独成段。
|
||||
- 翻译:上面样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄。
|
||||
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确。
|
||||
- 行替换:多行块替换后缩进正确。
|
||||
- 注册表:注册 / 注销后 `languageIds` 与可选翻译正确;同一语言多个翻译时按 `globalState` 选中。
|
||||
|
||||
@@ -82,4 +82,8 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
|
||||
emptyLiterals: { list: '空列表', dictionary: '空字典' },
|
||||
stringNodes: ['string'],
|
||||
commentText,
|
||||
// `a if c else b` 的三个具名子节点依次是 a、c、b,语法没给它们起字段名
|
||||
nodeTemplates: {
|
||||
conditional_expression: ['如果', { namedChild: 1 }, '则', { namedChild: 0 }, '否则', { namedChild: 2 }],
|
||||
},
|
||||
})
|
||||
@@ -88,4 +88,7 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
|
||||
emptyLiterals: { array: '空数组' },
|
||||
stringNodes: ['string', 'template_string', 'regex'],
|
||||
commentText,
|
||||
nodeTemplates: {
|
||||
ternary_expression: ['如果', { field: 'condition' }, '则', { field: 'consequence' }, '否则', { field: 'alternative' }],
|
||||
},
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LanguageAdapter, Segment, Translator } from '../contract'
|
||||
import type { ZhTable } from './zhTemplate'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { createTypeScriptAdapter } from '../languages/typescript'
|
||||
@@ -79,7 +80,7 @@ describe('typeScript 中文直译', () => {
|
||||
expect(env[32]).toBe('公共 接口 EnvLockResult')
|
||||
expect(env[42]).toBe('尝试')
|
||||
expect(env[46]).toBe('捕获 (err)')
|
||||
expect(env[47]).toBe('result.failed.push({ path: f, reason: err 属于 Error ? err.message : String(err) })')
|
||||
expect(env[47]).toBe('result.failed.push({ path: f, reason: 如果 err 属于 Error 则 err.message 否则 String(err) })')
|
||||
expect(env[1]).toBe('.env file lock / unlock helpers. Recursively scans the main workspace folder for `.env*` files (skipping `node_modules`, `.git`, `.claude`) and chmods them to 0o444 (locked) or 0o644 (unlocked) in bulk. Backs the toolbar Lock toggle in `PanelHeader`. No persistent watcher — re-scan happens on every toggle, so newly created `.env` files are only picked up the next time the user clicks the button.')
|
||||
expect(issue[1]).toBe('导入 类型 { Issue } 来自 \'../gitea/types\'')
|
||||
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
|
||||
@@ -113,7 +114,7 @@ describe('typeScript 中文直译', () => {
|
||||
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')
|
||||
// eslint-disable-next-line no-template-curly-in-string -- 模板字符串原样照抄进译文
|
||||
expect(map[3]).toBe('常量 s: 字符串 | 无 = 类型为 v 全等 \'string\' ? `${v}` : \'number\'')
|
||||
expect(map[3]).toBe('常量 s: 字符串 | 无 = 如果 类型为 v 全等 \'string\' 则 `${v}` 否则 \'number\'')
|
||||
})
|
||||
|
||||
it('多行合成一行、去掉右括号前的尾逗号、行中注释不进译文', async () => {
|
||||
@@ -158,6 +159,28 @@ describe('typeScript 中文直译', () => {
|
||||
expect(map[11]).toBe('公共 默认 函数 g()')
|
||||
expect(map[13]).toBe('默认情况')
|
||||
})
|
||||
|
||||
it('三元表达式按「如果…则…否则…」重排,嵌套的、槽位里的关键字和运算符照样翻', async () => {
|
||||
const source = [
|
||||
'err instanceof Error ? err.message : String(err)',
|
||||
'z = x ? (y ? 1 : 2) : 3',
|
||||
'const w = a ? b : c ? d : e',
|
||||
'const v = a !== b && !c ? typeof d : await e',
|
||||
].join('\n')
|
||||
const map = await translateAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
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',
|
||||
})
|
||||
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword')).toEqual([
|
||||
{ text: '如果', role: 'keyword' },
|
||||
{ text: '则', role: 'keyword' },
|
||||
{ text: '否则', role: 'keyword' },
|
||||
])
|
||||
expectWordsApart(map)
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 中文直译', () => {
|
||||
@@ -196,7 +219,7 @@ describe('python 中文直译', () => {
|
||||
36: 'failed: 字典[字符串, 字符串] = 空字典',
|
||||
37: 'label = \'env\'',
|
||||
39: '函数 summary(self, verbose: 布尔 = 假) -> 字符串',
|
||||
40: '返回 f\'{self.total} files\' 如果 verbose 不是 空 否则 \'\'',
|
||||
40: '返回 如果 verbose 不是 空 则 f\'{self.total} files\' 否则 \'\'',
|
||||
})
|
||||
expectWordsApart(map)
|
||||
})
|
||||
@@ -250,19 +273,39 @@ describe('python 中文直译', () => {
|
||||
expect(map[10]).toBe('未中断时')
|
||||
expect(map[16]).toBe('未出错时')
|
||||
})
|
||||
|
||||
it('条件表达式按「如果…则…否则…」重排,嵌套的、槽位里的关键字和运算符照样翻', async () => {
|
||||
const source = [
|
||||
'_default_port = 5680 if _role == "beat" else 5679',
|
||||
'y = a if x else b if y else c',
|
||||
'z = (a if x else b) if y else c',
|
||||
'v = [k for k in items if k not in seen] if not done and ok else None',
|
||||
].join('\n')
|
||||
const map = await translateAll(py, 'python', pythonZhTranslator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
1: '_default_port = 如果 _role 等于 "beat" 则 5680 否则 5679',
|
||||
2: 'y = 如果 x 则 a 否则 如果 y 则 b 否则 c',
|
||||
3: 'z = 如果 y 则 (如果 x 则 a 否则 b) 否则 c',
|
||||
4: 'v = 如果 非 done 与 ok 则 [k 遍历 k 取自 items 如果 k 不在 seen] 否则 空',
|
||||
})
|
||||
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
|
||||
{ text: '如果', role: 'keyword' },
|
||||
{ text: '等于', role: 'operator' },
|
||||
{ text: '则', role: 'keyword' },
|
||||
{ text: '否则', role: 'keyword' },
|
||||
])
|
||||
expectWordsApart(map)
|
||||
})
|
||||
})
|
||||
|
||||
describe('中文模板引擎', () => {
|
||||
it('关键字按父节点类型选译法,更长的节点类型链优先,都不匹配用默认', async () => {
|
||||
const translator = createZhTemplateTranslator({
|
||||
function testTranslator(table: Partial<ZhTable>): Translator {
|
||||
return 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': '戊' } },
|
||||
},
|
||||
keywords: {},
|
||||
literals: {},
|
||||
builtinTypes: {},
|
||||
isTypePosition: () => false,
|
||||
@@ -271,6 +314,17 @@ describe('中文模板引擎', () => {
|
||||
emptyLiterals: {},
|
||||
stringNodes: [],
|
||||
commentText: node => node.text,
|
||||
nodeTemplates: {},
|
||||
...table,
|
||||
})
|
||||
}
|
||||
|
||||
it('关键字按父节点类型选译法,更长的节点类型链优先,都不匹配用默认', async () => {
|
||||
const translator = testTranslator({
|
||||
keywords: {
|
||||
in: { zh: '甲', byContext: { for_statement: '乙' } },
|
||||
else: { zh: '丙', byContext: { 'else_clause': '丁', 'for_statement > else_clause': '戊' } },
|
||||
},
|
||||
})
|
||||
const source = [
|
||||
'for k in items:',
|
||||
@@ -288,4 +342,54 @@ describe('中文模板引擎', () => {
|
||||
expect(map[5]).toBe('x = a if c 丙 b')
|
||||
expect(map[6]).toBe('戊')
|
||||
})
|
||||
|
||||
it('节点模板按节点类型重排:槽位按字段名或具名子节点序号取子节点并递归翻译,译词按关键字着色', async () => {
|
||||
const translator = testTranslator({
|
||||
tokenRole: () => 'variable',
|
||||
keywords: { in: '在', else: { zh: '丙', byContext: { else_clause: '丁' } } },
|
||||
operators: { '==': '等于' },
|
||||
operatorParents: ['comparison_operator'],
|
||||
nodeTemplates: {
|
||||
conditional_expression: ['若', { namedChild: 1 }, '则', { namedChild: 0 }, '不然', { namedChild: 2 }],
|
||||
assignment: ['令', { field: 'left' }, '类型', { field: 'type' }, '为', { field: 'right' }],
|
||||
},
|
||||
})
|
||||
const source = [
|
||||
'n: int = a if k in seen else b',
|
||||
'print(a if c else b if d else e)',
|
||||
'w = (a # 注释不占序号',
|
||||
' if k == 1 else b)',
|
||||
].join('\n')
|
||||
const map = await translateAll(py, 'python', translator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
1: '令 n 类型 int 为 若 k 在 seen 则 a 不然 b',
|
||||
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
|
||||
3: 'w = (若 k 等于 1 则 a 不然 b)',
|
||||
})
|
||||
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword').map(segment => segment.text))
|
||||
.toEqual(['令', '类型', '为', '若', '则', '不然'])
|
||||
})
|
||||
|
||||
it('没命中模板的节点照抄源码结构;模板引用的子节点不存在时也不重排', async () => {
|
||||
const translator = testTranslator({
|
||||
keywords: { in: '在', else: { zh: '丙', byContext: { else_clause: '丁' } } },
|
||||
nodeTemplates: {
|
||||
assignment: ['令', { field: 'left' }, '类型', { field: 'type' }, '为', { field: 'right' }],
|
||||
},
|
||||
})
|
||||
const source = [
|
||||
'x = a if k in seen else b',
|
||||
'if k in seen:',
|
||||
' pass',
|
||||
'else:',
|
||||
' pass',
|
||||
].join('\n')
|
||||
expect(texts(await translateAll(py, 'python', translator, source))).toEqual({
|
||||
1: 'x = a if k 在 seen 丙 b',
|
||||
2: 'if k 在 seen',
|
||||
3: 'pass',
|
||||
4: '丁',
|
||||
5: 'pass',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,13 @@ export interface ContextualWord {
|
||||
byContext: Record<string, string>
|
||||
}
|
||||
|
||||
/** 一种语言的中文直译表。引擎只替换表里的三类东西,其余原样照抄。 */
|
||||
/**
|
||||
* 节点模板的一项。字符串是插入的译词,按关键字着色;另外两种是子节点槽位:
|
||||
* `{ field }` 按 tree-sitter 字段名取子节点,`{ namedChild }` 按具名子节点序号(0 起,不计注释)取,给语法没定义字段名的节点用。
|
||||
*/
|
||||
export type TemplatePart = string | { field: string } | { namedChild: number }
|
||||
|
||||
/** 一种语言的中文直译表。引擎替换表里的关键字、内置类型、运算符,按节点模板重排少数语法结构,其余原样照抄。 */
|
||||
export interface ZhTable {
|
||||
id: string
|
||||
label: string
|
||||
@@ -35,16 +41,29 @@ export interface ZhTable {
|
||||
stringNodes: string[]
|
||||
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行。 */
|
||||
commentText: (node: Node) => string
|
||||
/**
|
||||
* 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出;
|
||||
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
|
||||
*/
|
||||
nodeTemplates: Record<string, TemplatePart[]>
|
||||
}
|
||||
|
||||
interface Token {
|
||||
start: number
|
||||
end: number
|
||||
text: string
|
||||
role: TokenRole
|
||||
translated: boolean
|
||||
/** 源码里的原文,用来认出逗号、括号。 */
|
||||
/** 源码里的原文,用来认出逗号、括号;模板插入的译词没有原文。 */
|
||||
raw: string
|
||||
/** 前面要空一格:源码里它前面有空白,或者模板在它前面插了别的项。 */
|
||||
gap: boolean
|
||||
}
|
||||
|
||||
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
|
||||
interface Collector {
|
||||
source: string
|
||||
tokens: Token[]
|
||||
cursor: number
|
||||
gap: boolean
|
||||
}
|
||||
|
||||
const CONTEXT_SEPARATOR = ' > '
|
||||
@@ -77,6 +96,13 @@ function oneLine(text: string): string {
|
||||
return text.replace(/\s*\n\s*/g, ' ')
|
||||
}
|
||||
|
||||
/** 模板槽位引用的子节点;注释是语法之外的节点,不占具名子节点序号。 */
|
||||
function slotChild(node: Node, slot: Exclude<TemplatePart, string>): Node | null | undefined {
|
||||
if ('field' in slot)
|
||||
return node.childForFieldName(slot.field)
|
||||
return node.namedChildren.filter(child => child && !child.isExtra)[slot.namedChild]
|
||||
}
|
||||
|
||||
export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
const operatorParents = new Set(table.operatorParents)
|
||||
const stringNodes = new Set(table.stringNodes)
|
||||
@@ -120,22 +146,62 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function collect(node: Node, span: SourceSpan, out: Token[]): void {
|
||||
/** 节点命中模板时,模板各项换成译词或子节点;不该重排时返回 undefined。 */
|
||||
function templateParts(node: Node, span: SourceSpan): (string | Node)[] | undefined {
|
||||
const template = table.nodeTemplates[node.type]
|
||||
if (!template || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
|
||||
return undefined
|
||||
const parts = template.map(part => typeof part === 'string' ? part : slotChild(node, part))
|
||||
return parts.every((part): part is string | Node => part != null) ? parts : undefined
|
||||
}
|
||||
|
||||
/** 源码里上一个词到 index 之间有空白,下一个词前面就空一格。 */
|
||||
function advance(out: Collector, index: number): void {
|
||||
out.gap ||= /\s/.test(out.source.slice(out.cursor, index))
|
||||
out.cursor = index
|
||||
}
|
||||
|
||||
function emit(out: Collector, token: Omit<Token, 'gap'>): void {
|
||||
out.tokens.push({ ...token, gap: out.gap })
|
||||
out.gap = false
|
||||
}
|
||||
|
||||
function emitSource(out: Collector, node: Node, token: Omit<Token, 'gap' | 'raw'>): void {
|
||||
advance(out, node.startIndex)
|
||||
emit(out, { ...token, raw: node.text })
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
function collect(node: Node, span: SourceSpan, out: Collector): void {
|
||||
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
|
||||
return
|
||||
const base = { start: node.startIndex, end: node.endIndex }
|
||||
if (node.type === 'comment') {
|
||||
out.push({ ...base, raw: node.text, text: '', role: 'comment', translated: false })
|
||||
emitSource(out, node, { text: '', role: 'comment', translated: false })
|
||||
return
|
||||
}
|
||||
const parts = templateParts(node, span)
|
||||
if (parts) {
|
||||
advance(out, node.startIndex)
|
||||
parts.forEach((part, i) => {
|
||||
out.gap ||= i > 0
|
||||
if (typeof part === 'string') {
|
||||
emit(out, { text: part, role: 'keyword', translated: true, raw: '' })
|
||||
return
|
||||
}
|
||||
out.cursor = part.startIndex
|
||||
collect(part, span, out)
|
||||
})
|
||||
out.cursor = node.endIndex
|
||||
return
|
||||
}
|
||||
const translated = translateNode(node)
|
||||
if (translated) {
|
||||
out.push({ ...base, raw: node.text, ...translated, translated: true })
|
||||
emitSource(out, node, { ...translated, translated: true })
|
||||
return
|
||||
}
|
||||
if (node.childCount === 0 || stringNodes.has(node.type)) {
|
||||
const role = stringNodes.has(node.type) ? 'string' : table.tokenRole(node)
|
||||
out.push({ ...base, raw: node.text, text: oneLine(node.text), role, translated: false })
|
||||
emitSource(out, node, { text: oneLine(node.text), role, translated: false })
|
||||
return
|
||||
}
|
||||
for (const child of node.children) {
|
||||
@@ -155,8 +221,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
function translate(block: CodeBlock, source: string): Segment[] {
|
||||
if (block.kind === 'comment')
|
||||
return translateComment(block)
|
||||
const tokens: Token[] = []
|
||||
collect(block.node, block.header, tokens)
|
||||
const out: Collector = { source, tokens: [], cursor: block.header.startIndex, gap: false }
|
||||
collect(block.node, block.header, out)
|
||||
const { tokens } = out
|
||||
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
|
||||
const last = tokens.at(-1)
|
||||
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
|
||||
@@ -171,10 +238,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
segments.push({ text, role })
|
||||
}
|
||||
let gap = false
|
||||
let prevEnd = block.header.startIndex
|
||||
tokens.forEach((token, i) => {
|
||||
gap ||= /\s/.test(source.slice(prevEnd, token.start))
|
||||
prevEnd = token.end
|
||||
gap ||= token.gap
|
||||
const trailingComma = token.raw === ',' && !token.translated && CLOSING.has(nextRaw(tokens, i))
|
||||
if (token.text === '' || trailingComma) {
|
||||
gap ||= token.role === 'comment'
|
||||
|
||||
Reference in new issue
Block a user