import type { Access, FactBlock, FunctionFacts, ImportFacts, ImportItem, Node, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract' /** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */ export interface ContextualWord { /** 没有匹配的语境时用的译法。 */ zh: string /** * 语境 → 译法。语境是关键字节点往上的节点类型链,外层在前、用 ` > ` 连接: * `raise_statement` 指父节点,`for_statement > else_clause` 指父节点 else_clause 且祖父节点 for_statement。 * 同时匹配多条时取最长的那条。 */ byContext: Record } /** * 节点模板的一项。字符串是插入的译词,按关键字着色;另外两种是子节点槽位: * `{ field }` 按 tree-sitter 字段名取子节点,`{ namedChild }` 按具名子节点序号(0 起,不计注释)取,给语法没定义字段名的节点用。 */ export type TemplatePart = string | { field: string } | { namedChild: number } /** 一种语言的中文直译表。引擎替换表里的关键字、内置类型、运算符,按节点模板重排少数语法结构,其余原样照抄。 */ export interface ZhTable { id: string label: string languageIds: string[] /** 未翻译的词按源码着色的词性上色;关键字译文的词性也取它。 */ tokenRole: (leaf: Node) => TokenRole /** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点);随所在语法节点换译法的写成 ContextualWord。 */ keywords: Record /** 字面量关键字:具名的叶子节点(true / None …),按节点类型查。 */ literals: Record /** 内置类型:只在 isTypePosition 成立的叶子上按原文查。 */ builtinTypes: Record isTypePosition: (leaf: Node) => boolean /** 翻成文字的运算符:只认父节点在 operatorParents 里的运算符节点(TS 的非空断言 `x!` 不算)。 */ operators: Record operatorParents: string[] /** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典。 */ emptyLiterals: Record /** 整段原样照抄、按字符串着色的节点。 */ stringNodes: string[] /** 注释(或 docstring)节点去掉注释符号后的原文,可以多行。 */ commentText: (node: Node) => string /** * 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出; * 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。 */ nodeTemplates: Record } interface Token { text: string role: TokenRole translated: boolean /** 源码里的原文,用来认出逗号、括号;模板插入的译词没有原文。 */ raw: string /** 前面要空一格:源码里它前面有空白,或者模板在它前面插了别的项。 */ gap: boolean /** 照抄的名字、字符串在源码里的起始下标,见 Segment.offset。 */ offset?: number } /** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */ interface Collector { source: string tokens: Token[] cursor: number gap: boolean } const CONTEXT_SEPARATOR = ' > ' const CLOSING = new Set([')', ']', '}']) const HEADER_OPENERS = new Set(['{', ':']) /** 源码着色里代表名字的词性:着这几种色的具名叶子是标识符、属性名、类型名,能查定义。 */ const NAME_ROLES = new Set(['variable', 'function', 'type']) function firstLeaf(node: Node): Node { const first = node.child(0) return first ? firstLeaf(first) : node } function commentsWithin(node: Node, span: SourceSpan): Node[] { if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex) return [] if (node.type === 'comment') return [node] return node.children.flatMap(child => child ? commentsWithin(child, span) : []) } /** 下一个要输出的词的原文(跳过注释)。 */ function nextRaw(tokens: Token[], i: number): string { for (let j = i + 1; j < tokens.length; j++) { if (tokens[j].text !== '') return tokens[j].raw } return '' } function oneLine(text: string): string { return text.replace(/\s*\n\s*/g, ' ') } /** 模板槽位引用的子节点;注释是语法之外的节点,不占具名子节点序号。 */ function slotChild(node: Node, slot: Exclude): 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) 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 offsetOf(node: Node): number | undefined { const linkable = stringNodes.has(node.type) || (node.childCount === 0 && node.isNamed && NAME_ROLES.has(table.tokenRole(node))) return linkable ? node.startIndex : undefined } /** 能整个换成中文的节点 → 译文和词性。 */ function translateNode(node: Node): { text: string, role: TokenRole } | undefined { if (node.childCount === 0 && table.isTypePosition(node)) { const type = table.builtinTypes[node.text] if (type) return { text: type, role: 'type' } } const parent = node.parent if (!node.isNamed) { 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) return { text: keywordText(word, node), role: table.tokenRole(firstLeaf(node)) } return undefined } if (node.childCount === 0 && table.literals[node.type]) return { text: table.literals[node.type], role: table.tokenRole(node) } if (table.emptyLiterals[node.type] && node.namedChildCount === 0) return { text: table.emptyLiterals[node.type], role: 'literal' } return undefined } /** 节点命中模板时,模板各项换成译词或子节点;不该重排时返回 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): void { out.tokens.push({ ...token, gap: out.gap }) out.gap = false } function emitSource(out: Collector, node: Node, token: Omit): 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 if (node.type === 'comment') { 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) { 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) emitSource(out, node, { text: oneLine(node.text), role, translated: false, offset: offsetOf(node) }) return } for (const child of node.children) { if (child) collect(child, span, out) } } /** 按译文顺序收集 node 落在 span 里的词。 */ function tokensIn(node: Node, span: SourceSpan, source: string): Token[] { const out: Collector = { source, tokens: [], cursor: span.startIndex, gap: false } collect(node, span, out) return out.tokens } /** node 的源码原样照抄:叶子按源码着色(给了 role 就都按它),叶子之间的空白、标点照抄。 */ function verbatim(node: Node, source: string, role?: TokenRole): Segment[] { const segments: Segment[] = [] let cursor = node.startIndex const visit = (current: Node): void => { if (current.startIndex === current.endIndex) return if (current.childCount > 0) { for (const child of current.children) { if (child) visit(child) } return } pushSegment(segments, source.slice(cursor, current.startIndex), 'plain') pushSegment(segments, current.text, role ?? table.tokenRole(current), offsetOf(current)) cursor = current.endIndex } visit(node) pushSegment(segments, source.slice(cursor, node.endIndex), 'plain') return segments } function translateComment(block: TextBlock): Segment[] { const { startIndex, endIndex } = block.header const root = block.node.tree.rootNode const nodes = block.node.type === 'comment' ? commentsWithin(root.descendantForIndex(startIndex, endIndex) ?? root, block.header) : [block.node] const text = nodes.map(table.commentText).join('\n').split('\n').map(line => line.trim()).filter(Boolean).join(' ') return text ? [{ text, role: 'comment' }] : [] } function translate(block: TextBlock, source: string): Segment[] { if (block.kind === 'comment') return translateComment(block) const tokens = tokensIn(block.node, block.header, source) 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)) tokens.pop() return joinTokens(tokens) } function describe(block: FactBlock, source: string): PropertyRow[] { const { header } = block const phrases: Phrases = { // 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致 translated: (node) => { const clipped = node.endIndex > header.endIndex const span = { startIndex: node.startIndex, endIndex: clipped ? header.endIndex : node.endIndex, startRow: node.startPosition.row, endRow: clipped ? header.endRow : node.endPosition.row, } return joinTokens(tokensIn(node, span, source)) }, verbatim: node => verbatim(node, source), // 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样 module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node))] : verbatim(node, source, 'type'), name: (node, role) => [segment(node.text, role, offsetOf(node))], } switch (block.kind) { case 'function': return functionRows(block.facts, phrases) case 'import': return importRows(block.facts, phrases) default: return variableRows(block, phrases) } } return { id: table.id, label: table.label, languageIds: table.languageIds, translate, describe } } function segment(text: string, role: TokenRole, offset: number | undefined): Segment { return offset === undefined ? { text, role } : { text, role, offset } } function pushSegment(segments: Segment[], text: string, role: TokenRole, offset?: number): void { if (!text) return const prev = segments.at(-1) if (role === 'plain' && prev?.role === 'plain') prev.text += text else segments.push(segment(text, role, offset)) } /** 词连成译文:源码里隔着空白的词之间空一格,注释不进译文,右括号前的尾逗号去掉。 */ function joinTokens(tokens: Token[]): Segment[] { const segments: Segment[] = [] let gap = false tokens.forEach((token, i) => { gap ||= token.gap const trailingComma = token.raw === ',' && !token.translated && CLOSING.has(nextRaw(tokens, i)) if (token.text === '' || trailingComma) { gap ||= token.role === 'comment' return } if (gap && segments.length > 0) pushSegment(segments, ' ', 'plain') pushSegment(segments, token.text, token.role, token.offset) // 「非」是个词,后面补一个空格,不和操作数粘在一起 gap = token.translated && token.raw === '!' }) return segments } // ─── 属性表 ─────────────────────────────────────────────────────────────────── /** 属性表里值的三种来源。 */ interface Phrases { /** 按表翻译,只翻落在块头部里的那一段。 */ translated: (node: Node) => Segment[] /** 源码原样照抄、按源码着色(装饰器)。 */ verbatim: (node: Node) => Segment[] /** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */ module: (node: Node) => Segment[] /** 源码里的一个名字原样照抄、按 role 着色(函数名称)。 */ name: (node: Node, role: TokenRole) => Segment[] } const ACCESS_ZH: Record = { public: '公共', private: '私有', protected: '受保护' } function hint(text: string): Segment { return { text, role: 'hint' } } function yesNo(value: boolean): Segment[] { return [{ text: value ? '是' : '否', role: 'storage' }] } function count(n: number): Segment[] { return [hint(n > 0 ? `${n} 个` : '无')] } function joined(parts: Segment[][], separator: string): Segment[] { return parts.flatMap((part, i) => i > 0 ? [{ text: separator, role: 'plain' as const }, ...part] : part) } function accessRows(access: Access | undefined): PropertyRow[] { return access ? [{ label: '访问级别', value: [{ text: ACCESS_ZH[access], role: 'keyword' }] }] : [] } function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segment[] { if (!alias) return item return [...item, { text: ' ', role: 'plain' }, { text: '作为', role: 'keyword' }, { text: ' ', role: 'plain' }, ...say.translated(alias)] } /** 函数:名称 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] { const decorators = facts.decorators.map(say.verbatim) return [ { label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] }, ...accessRows(facts.access), { label: '异步', value: yesNo(facts.async) }, ...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []), { label: '输入参数', value: count(facts.parameters.length), items: facts.parameters.map(param => ({ cells: [ param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name), param.type ? say.translated(param.type) : [hint('未标注')], param.default ? [hint('默认 '), ...say.translated(param.default)] : [], ], })), }, { label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] }, ] } /** 导入:来源 / 仅类型(只有分这个的语言才有)/ 导入项(每项一行,有别名写成「名称 作为 别名」)。 */ function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] { const itemText = (item: ImportItem): Segment[] => { switch (item.kind) { case 'named': return aliased(say.translated(item.name), item.alias, say) case 'default': return aliased([hint('默认导出')], item.alias, say) case 'namespace': return aliased([hint('全部')], item.alias, say) case 'module': // 一条语句导入多个模块时(Python 的 import a, b)要写出是哪个模块 return aliased(facts.sources.length > 1 ? say.module(item.module) : [hint('整个模块')], item.alias, say) } } return [ { label: '来源', value: facts.sources.length > 0 ? joined(facts.sources.map(say.module), ', ') : [hint('无')] }, ...(facts.typeOnly === undefined ? [] : [{ label: '仅类型', value: yesNo(facts.typeOnly) }]), { label: '导入项', value: count(facts.items.length), items: facts.items.map(item => ({ cells: [itemText(item)] })) }, ] } /** 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值)。 */ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] { const valueLabel = block.kind === 'const' ? '值' : '初始值' return block.facts.declarators.flatMap(declarator => [ { label: '名称', value: say.translated(declarator.name) }, ...accessRows(block.facts.access), ...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []), { label: valueLabel, value: declarator.value ? say.translated(declarator.value) : [hint('无')] }, ]) }