import {ITranslationMessagesFile} from '../api/i-translation-messages-file'; import {ITransUnit} from '../api/i-trans-unit'; import {FORMAT_XLIFF20, FILETYPE_XLIFF20} from '../api/constants'; import {DOMUtilities} from './dom-utilities'; import {Xliff2TransUnit} from './xliff2-trans-unit'; import {AbstractTranslationMessagesFile} from './abstract-translation-messages-file'; import {AbstractTransUnit} from './abstract-trans-unit'; /** * Created by martin on 04.05.2017. * An XLIFF 2.0 file read from a source file. * Format definition is: http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html * * Defines some relevant get and set method for reading and modifying such a file. */ export class Xliff2File extends AbstractTranslationMessagesFile implements ITranslationMessagesFile { /** * Create an XLIFF 2.0-File from source. * @param xmlString source read from file. * @param path Path to file * @param encoding optional encoding of the xml. * This is read from the file, but if you know it before, you can avoid reading the file twice. * @return xliff file */ constructor(xmlString: string, path: string, encoding: string) { super(); this._warnings = []; this._numberOfTransUnitsWithMissingId = 0; this.initializeFromContent(xmlString, path, encoding); } private initializeFromContent(xmlString: string, path: string, encoding: string): Xliff2File { this.parseContent(xmlString, path, encoding); const xliffList = this._parsedDocument.getElementsByTagName('xliff'); if (xliffList.length !== 1) { throw new Error(`File "${path}" seems to be no xliff file (should contain an xliff element)`); } else { const version = xliffList.item(0).getAttribute('version'); const expectedVersion = '2.0'; if (version !== expectedVersion) { throw new Error(`File "${path}" seems to be no xliff 2 file, version should be ${expectedVersion}, found ${version}`); } } return this; } /** * File format as it is used in config files. * Currently 'xlf', 'xmb', 'xmb2' * Returns one of the constants FORMAT_.. */ public i18nFormat(): string { return FORMAT_XLIFF20; } /** * File type. * Here 'XLIFF 2.0' */ public fileType(): string { return FILETYPE_XLIFF20; } /** * return tag names of all elements that have mixed content. * These elements will not be beautified. * Typical candidates are source and target. */ protected elementsWithMixedContent(): string[] { return ['skeleton', 'note', 'data', 'source', 'target', 'pc', 'mrk']; } protected initializeTransUnits() { this.transUnits = []; const transUnitsInFile = this._parsedDocument.getElementsByTagName('unit'); for (let i = 0; i < transUnitsInFile.length; i++) { const transunit = transUnitsInFile.item(i); const id = transunit.getAttribute('id'); if (!id) { this._warnings.push(`oops, trans-unit without "id" found in master, please check file ${this._filename}`); } this.transUnits.push(new Xliff2TransUnit(transunit, id, this)); } } /** * Get source language. * @return source language. */ public sourceLanguage(): string { const xliffElem = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'xliff'); if (xliffElem) { return xliffElem.getAttribute('srcLang'); } else { return null; } } /** * Edit the source language. * @param language language */ public setSourceLanguage(language: string) { const xliffElem = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'xliff'); if (xliffElem) { xliffElem.setAttribute('srcLang', language); } } /** * Get target language. * @return target language. */ public targetLanguage(): string { const xliffElem = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'xliff'); if (xliffElem) { return xliffElem.getAttribute('trgLang'); } else { return null; } } /** * Edit the target language. * @param language language */ public setTargetLanguage(language: string) { const xliffElem = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'xliff'); if (xliffElem) { xliffElem.setAttribute('trgLang', language); } } /** * Add a new trans-unit to this file. * The trans unit stems from another file. * It copies the source content of the tu to the target content too, * depending on the values of isDefaultLang and copyContent. * So the source can be used as a dummy translation. * (used by xliffmerge) * @param foreignTransUnit the trans unit to be imported. * @param isDefaultLang Flag, wether file contains the default language. * Then source and target are just equal. * The content will be copied. * State will be final. * @param copyContent Flag, wether to copy content or leave it empty. * Wben true, content will be copied from source. * When false, content will be left empty (if it is not the default language). * @param importAfterElement optional (since 1.10) other transunit (part of this file), that should be used as ancestor. * Newly imported trans unit is then inserted directly after this element. * If not set or not part of this file, new unit will be imported at the end. * If explicity set to null, new unit will be imported at the start. * @return the newly imported trans unit (since version 1.7.0) * @throws an error if trans-unit with same id already is in the file. */ importNewTransUnit(foreignTransUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean, importAfterElement?: ITransUnit) : ITransUnit { if (this.transUnitWithId(foreignTransUnit.id)) { throw new Error(`tu with id ${foreignTransUnit.id} already exists in file, cannot import it`); } const newTu = ( foreignTransUnit).cloneWithSourceAsTarget(isDefaultLang, copyContent, this); const fileElement = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'file'); if (!fileElement) { throw new Error(`File "${this._filename}" seems to be no xliff 2.0 file (should contain a file element)`); } let inserted = false; let isAfterElementPartOfFile = false; if (!!importAfterElement) { const insertionPoint = this.transUnitWithId(importAfterElement.id); if (!!insertionPoint) { isAfterElementPartOfFile = true; } } if (importAfterElement === undefined || (importAfterElement && !isAfterElementPartOfFile)) { fileElement.appendChild(newTu.asXmlElement()); inserted = true; } else if (importAfterElement === null) { const firstUnitElement = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'unit'); if (firstUnitElement) { DOMUtilities.insertBefore(newTu.asXmlElement(), firstUnitElement); inserted = true; } else { // no trans-unit, empty file, so add to first file element fileElement.appendChild(newTu.asXmlElement()); inserted = true; } } else { const refUnitElement = DOMUtilities.getElementByTagNameAndId(this._parsedDocument, 'unit', importAfterElement.id); if (refUnitElement) { DOMUtilities.insertAfter(newTu.asXmlElement(), refUnitElement); inserted = true; } } if (inserted) { this.lazyInitializeTransUnits(); this.transUnits.push(newTu); this.countNumbers(); return newTu; } else { return null; } } /** * Create a new translation file for this file for a given language. * Normally, this is just a copy of the original one. * But for XMB the translation file has format 'XTB'. * @param lang Language code * @param filename expected filename to store file * @param isDefaultLang Flag, wether file contains the default language. * Then source and target are just equal. * The content will be copied. * State will be final. * @param copyContent Flag, wether to copy content or leave it empty. * Wben true, content will be copied from source. * When false, content will be left empty (if it is not the default language). */ public createTranslationFileForLang(lang: string, filename: string, isDefaultLang: boolean, copyContent: boolean) : ITranslationMessagesFile { const translationFile = new Xliff2File(this.editedContent(), filename, this.encoding()); translationFile.setNewTransUnitTargetPraefix(this.targetPraefix); translationFile.setNewTransUnitTargetSuffix(this.targetSuffix); translationFile.setTargetLanguage(lang); translationFile.forEachTransUnit((transUnit: ITransUnit) => { ( transUnit).useSourceAsTarget(isDefaultLang, copyContent); }); return translationFile; } }