✨ feat(vscode): 条件表达式按「如果…则…否则…」翻译

This commit is contained in:
cruldra committed 2026-09-25 03:19:42 +08:00
1 parent 3a184eed9b
commit 6897954644
5 files changed
+212 -27

No files matched your search

+79 -14
View File
@@ -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'