UNPKG

xliff-mcp

Version:

MCP server for XLIFF bilingual file processing

315 lines (314 loc) 14.1 kB
import { DOMParser, XMLSerializer } from 'xmldom'; export class XliffProcessor { /** * 解析XLIFF文件内容为JSON数据 */ static parseXliff(fileName, content) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(content, 'text/xml'); // 检查XML解析错误 const parseError = xmlDoc.getElementsByTagName('parsererror')[0]; if (parseError) { throw new Error(`XML解析错误: ${parseError.textContent}`); } // 支持XLIFF 1.2和2.x版本,使用getElementsByTagName代替querySelectorAll const transUnits1 = xmlDoc.getElementsByTagName('trans-unit'); const units2 = xmlDoc.getElementsByTagName('unit'); // 合并两种类型的单元 const allUnits = []; for (let i = 0; i < transUnits1.length; i++) { allUnits.push(transUnits1[i]); } for (let i = 0; i < units2.length; i++) { allUnits.push(units2[i]); } const data = []; // 获取文件级别的语言信息 const xliffElement = xmlDoc.getElementsByTagName('xliff')[0]; const fileElement = xmlDoc.getElementsByTagName('file')[0]; let defaultSrcLang = ''; let defaultTgtLang = ''; if (fileElement) { defaultSrcLang = fileElement.getAttribute('source-language') || ''; defaultTgtLang = fileElement.getAttribute('target-language') || ''; } else if (xliffElement) { defaultSrcLang = xliffElement.getAttribute('srcLang') || ''; defaultTgtLang = xliffElement.getAttribute('trgLang') || ''; } // 处理每个翻译单元 for (let index = 0; index < allUnits.length; index++) { const unit = allUnits[index]; const id = unit.getAttribute('id') || `${index + 1}`; const percent = unit.getAttribute('percent') || unit.getAttribute('mq:percent') || unit.getAttribute('match-quality') || '-1'; // 获取source和target元素 const sourceElements = unit.getElementsByTagName('source'); const targetElements = unit.getElementsByTagName('target'); const sourceElement = sourceElements.length > 0 ? sourceElements[0] : null; const targetElement = targetElements.length > 0 ? targetElements[0] : null; const source = sourceElement?.textContent || ''; const target = targetElement?.textContent || ''; // 获取语言信息(优先使用单元级别的设置) const srcLang = unit.getAttribute('source-language')?.toLowerCase() || sourceElement?.getAttribute('xml:lang')?.toLowerCase() || defaultSrcLang.toLowerCase(); const tgtLang = unit.getAttribute('target-language')?.toLowerCase() || targetElement?.getAttribute('xml:lang')?.toLowerCase() || defaultTgtLang.toLowerCase(); // 获取状态和批准信息 const state = targetElement?.getAttribute('state') || unit.getAttribute('state') || ''; const approved = targetElement?.getAttribute('approved') === 'yes' || unit.getAttribute('approved') === 'yes'; // 获取resname属性 const resname = unit.getAttribute('resname') || unit.getAttribute('name') || ''; if (id) { data.push({ fileName, segNumber: index + 1, percent: Number(percent), source, target, srcLang, tgtLang, id, approved, state, resname }); } } return data; } catch (error) { console.error(`解析XLIFF文件失败: ${error}`); throw new Error(`解析XLIFF文件失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 根据JSON数据创建XLIFF文件 */ static createXliff(data, options) { const version = options.version || '1.2'; if (version === '1.2') { return this.createXliff12(data, options); } else { return this.createXliff20(data, options); } } /** * 创建XLIFF 1.2格式文件 */ static createXliff12(data, options) { const original = options.original || 'source.txt'; const datatype = options.datatype || 'plaintext'; let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`; xml += `<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">\n`; xml += ` <file original="${this.escapeXml(original)}" source-language="${options.sourceLanguage}" target-language="${options.targetLanguage}" datatype="${datatype}">\n`; xml += ` <body>\n`; data.forEach(item => { xml += ` <trans-unit id="${this.escapeXml(item.id || item.segNumber.toString())}"`; if (item.resname) { xml += ` resname="${this.escapeXml(item.resname)}"`; } if (item.approved) { xml += ` approved="yes"`; } xml += `>\n`; xml += ` <source>${this.escapeXml(item.source)}</source>\n`; if (item.target) { xml += ` <target`; if (item.state) { xml += ` state="${this.escapeXml(item.state)}"`; } xml += `>${this.escapeXml(item.target)}</target>\n`; } xml += ` </trans-unit>\n`; }); xml += ` </body>\n`; xml += ` </file>\n`; xml += `</xliff>\n`; return xml; } /** * 创建XLIFF 2.0格式文件 */ static createXliff20(data, options) { const original = options.original || 'source.txt'; let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`; xml += `<xliff version="2.1" xmlns="urn:oasis:names:tc:xliff:document:2.1" srcLang="${options.sourceLanguage}" trgLang="${options.targetLanguage}">\n`; xml += ` <file id="f1" original="${this.escapeXml(original)}">\n`; data.forEach(item => { xml += ` <unit id="${this.escapeXml(item.id || item.segNumber.toString())}"`; if (item.resname) { xml += ` name="${this.escapeXml(item.resname)}"`; } xml += `>\n`; xml += ` <segment>\n`; xml += ` <source>${this.escapeXml(item.source)}</source>\n`; if (item.target) { xml += ` <target`; if (item.state) { xml += ` state="${this.escapeXml(item.state)}"`; } xml += `>${this.escapeXml(item.target)}</target>\n`; } xml += ` </segment>\n`; xml += ` </unit>\n`; }); xml += ` </file>\n`; xml += `</xliff>\n`; return xml; } /** * 更新现有XLIFF文件 */ static updateXliff(originalContent, updates, options) { try { const parser = new DOMParser(); const serializer = new XMLSerializer(); const xmlDoc = parser.parseFromString(originalContent, 'text/xml'); // 检查XML解析错误 const parseError = xmlDoc.getElementsByTagName('parsererror')[0]; if (parseError) { throw new Error(`XML解析错误: ${parseError.textContent}`); } // 使用getElementsByTagName代替querySelectorAll const transUnits1 = xmlDoc.getElementsByTagName('trans-unit'); const units2 = xmlDoc.getElementsByTagName('unit'); const allUnits = []; for (let i = 0; i < transUnits1.length; i++) { allUnits.push(transUnits1[i]); } for (let i = 0; i < units2.length; i++) { allUnits.push(units2[i]); } // 创建更新映射 const updateMap = new Map(); updates.forEach(update => { const key = update.id || update.segNumber.toString(); updateMap.set(key, update); }); // 更新现有单元 for (let index = 0; index < allUnits.length; index++) { const unit = allUnits[index]; const id = unit.getAttribute('id') || (index + 1).toString(); const update = updateMap.get(id); if (update) { const targetElements = unit.getElementsByTagName('target'); const targetElement = targetElements.length > 0 ? targetElements[0] : null; if (targetElement) { // 更新target内容 targetElement.textContent = update.target || ''; // 更新状态 if (update.state && !options.preserveState) { targetElement.setAttribute('state', update.state); } // 更新批准状态 if (update.approved !== undefined && !options.preserveApproval) { if (update.approved) { targetElement.setAttribute('approved', 'yes'); } else { targetElement.removeAttribute('approved'); } } } else if (update.target) { // 创建新的target元素 const newTarget = xmlDoc.createElement('target'); newTarget.textContent = update.target; if (update.state) { newTarget.setAttribute('state', update.state); } if (update.approved) { newTarget.setAttribute('approved', 'yes'); } const sourceElements = unit.getElementsByTagName('source'); const sourceElement = sourceElements.length > 0 ? sourceElements[0] : null; if (sourceElement && sourceElement.nextSibling) { unit.insertBefore(newTarget, sourceElement.nextSibling); } else { unit.appendChild(newTarget); } } // 更新source内容(如果需要) if (update.source) { const sourceElements = unit.getElementsByTagName('source'); const sourceElement = sourceElements.length > 0 ? sourceElements[0] : null; if (sourceElement) { sourceElement.textContent = update.source; } } } } return serializer.serializeToString(xmlDoc); } catch (error) { throw new Error(`更新XLIFF文件失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * XML转义 */ static escapeXml(text) { return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&apos;'); } /** * 验证XLIFF文件格式 */ static validateXliff(content) { const errors = []; try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(content, 'text/xml'); // 检查XML解析错误 const parseError = xmlDoc.getElementsByTagName('parsererror')[0]; if (parseError) { errors.push(`XML解析错误: ${parseError.textContent}`); return { valid: false, errors }; } // 检查根元素 const xliffElements = xmlDoc.getElementsByTagName('xliff'); if (xliffElements.length === 0) { errors.push('缺少xliff根元素'); } else { const xliffElement = xliffElements[0]; const version = xliffElement.getAttribute('version'); if (!version) { errors.push('xliff元素缺少version属性'); } else if (!['1.2', '2.0', '2.1'].includes(version)) { errors.push(`不支持的XLIFF版本: ${version}`); } } // 检查文件元素 const fileElements = xmlDoc.getElementsByTagName('file'); if (fileElements.length === 0) { errors.push('缺少file元素'); } // 检查翻译单元 const transUnits = xmlDoc.getElementsByTagName('trans-unit'); const units = xmlDoc.getElementsByTagName('unit'); if (transUnits.length === 0 && units.length === 0) { errors.push('没有找到翻译单元'); } return { valid: errors.length === 0, errors }; } catch (error) { errors.push(`验证失败: ${error instanceof Error ? error.message : String(error)}`); return { valid: false, errors }; } } }