xliff-mcp
Version:
MCP server for XLIFF bilingual file processing
315 lines (314 loc) • 14.1 kB
JavaScript
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* 验证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 };
}
}
}