@ngx-i18nsupport/ngx-i18nsupport-lib
Version:
A Typescript library to work with Angular generated i18n files (xliff, xmb)
257 lines (242 loc) • 11.7 kB
text/typescript
import {ITranslationMessagesFileFactory} from '../api/i-translation-messages-file-factory';
import {ITranslationMessagesFile} from '../api/i-translation-messages-file';
import {ITransUnit} from '../api/i-trans-unit';
import {FORMAT_XTB, FILETYPE_XTB, FORMAT_XMB} from '../api/constants';
import {format} from 'util';
import {DOMUtilities} from './dom-utilities';
import {AbstractTranslationMessagesFile} from './abstract-translation-messages-file';
import {XtbTransUnit} from './xtb-trans-unit';
import {AbstractTransUnit} from './abstract-trans-unit';
/**
* Created by martin on 23.05.2017.
* xtb-File access.
* xtb is the translated counterpart to xmb.
*/
export class XtbFile extends AbstractTranslationMessagesFile implements ITranslationMessagesFile {
// attached master file, if any
// used as source to determine state ...
private _masterFile: ITranslationMessagesFile; // an xmb-file
/**
* Create an xmb-File from source.
* @param _translationMessageFileFactory factory to create a translation file (xtb) for the xmb file
* @param xmlString file content
* @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.
* @param optionalMaster in case of xmb the master file, that contains the original texts.
* (this is used to support state infos, that are based on comparing original with translated version)
* @return XmbFile
*/
constructor(private _translationMessageFileFactory: ITranslationMessagesFileFactory,
xmlString: string, path: string, encoding: string,
optionalMaster?: { xmlContent: string, path: string, encoding: string }) {
super();
this._warnings = [];
this._numberOfTransUnitsWithMissingId = 0;
this.initializeFromContent(xmlString, path, encoding, optionalMaster);
}
private initializeFromContent(xmlString: string, path: string, encoding: string,
optionalMaster?: { xmlContent: string, path: string, encoding: string }): XtbFile {
this.parseContent(xmlString, path, encoding);
if (this._parsedDocument.getElementsByTagName('translationbundle').length !== 1) {
throw new Error(format('File "%s" seems to be no xtb file (should contain a translationbundle element)', path));
}
if (optionalMaster) {
try {
this._masterFile = this._translationMessageFileFactory.createFileFromFileContent(
FORMAT_XMB,
optionalMaster.xmlContent,
optionalMaster.path,
optionalMaster.encoding);
// check, wether this can be the master ...
const numberInMaster = this._masterFile.numberOfTransUnits();
const myNumber = this.numberOfTransUnits();
if (numberInMaster !== myNumber) {
this._warnings.push(format(
'%s trans units found in master, but this file has %s. Check if it is the correct master',
numberInMaster, myNumber));
}
} catch (error) {
throw new Error(format('File "%s" seems to be no xmb file. An xtb file needs xmb as master file.', optionalMaster.path));
}
}
return this;
}
protected initializeTransUnits() {
this.transUnits = [];
const transUnitsInFile = this._parsedDocument.getElementsByTagName('translation');
for (let i = 0; i < transUnitsInFile.length; i++) {
const msg = transUnitsInFile.item(i);
const id = msg.getAttribute('id');
if (!id) {
this._warnings.push(format('oops, msg without "id" found in master, please check file %s', this._filename));
}
let masterUnit: ITransUnit = null;
if (this._masterFile) {
masterUnit = this._masterFile.transUnitWithId(id);
}
this.transUnits.push(new XtbTransUnit(msg, id, this, <AbstractTransUnit> masterUnit));
}
}
/**
* File format as it is used in config files.
* Currently 'xlf', 'xlf2', 'xmb', 'xtb'
* Returns one of the constants FORMAT_..
*/
public i18nFormat(): string {
return FORMAT_XTB;
}
/**
* File type.
* Here 'XTB'
*/
public fileType(): string {
return FILETYPE_XTB;
}
/**
* 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 ['translation'];
}
/**
* Get source language.
* Unsupported in xmb/xtb.
* Try to guess it from master filename if any..
* @return source language.
*/
public sourceLanguage(): string {
if (this._masterFile) {
return this._masterFile.sourceLanguage();
} else {
return null;
}
}
/**
* Edit the source language.
* Unsupported in xmb/xtb.
* @param language language
*/
public setSourceLanguage(language: string) {
// do nothing, xtb has no notation for this.
}
/**
* Get target language.
* @return target language.
*/
public targetLanguage(): string {
const translationbundleElem = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'translationbundle');
if (translationbundleElem) {
return translationbundleElem.getAttribute('lang');
} else {
return null;
}
}
/**
* Edit the target language.
* @param language language
*/
public setTargetLanguage(language: string) {
const translationbundleElem = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'translationbundle');
if (translationbundleElem) {
translationbundleElem.setAttribute('lang', 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(format('tu with id %s already exists in file, cannot import it', foreignTransUnit.id));
}
const newMasterTu = (<AbstractTransUnit> foreignTransUnit).cloneWithSourceAsTarget(isDefaultLang, copyContent, this);
const translationbundleElem = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'translationbundle');
if (!translationbundleElem) {
throw new Error(format('File "%s" seems to be no xtb file (should contain a translationbundle element)', this._filename));
}
const translationElement = translationbundleElem.ownerDocument.createElement('translation');
translationElement.setAttribute('id', foreignTransUnit.id);
let newContent = (copyContent || isDefaultLang) ? foreignTransUnit.sourceContent() : '';
if (!(<AbstractTransUnit> foreignTransUnit).isICUMessage(newContent)) {
newContent = this.getNewTransUnitTargetPraefix() + newContent + this.getNewTransUnitTargetSuffix();
}
DOMUtilities.replaceContentWithXMLContent(translationElement, newContent);
const newTu = new XtbTransUnit(translationElement, foreignTransUnit.id, this, newMasterTu);
let inserted = false;
let isAfterElementPartOfFile = false;
if (!!importAfterElement) {
const insertionPoint = this.transUnitWithId(importAfterElement.id);
if (!!insertionPoint) {
isAfterElementPartOfFile = true;
}
}
if (importAfterElement === undefined || (importAfterElement && !isAfterElementPartOfFile)) {
translationbundleElem.appendChild(newTu.asXmlElement());
inserted = true;
} else if (importAfterElement === null) {
const firstTranslationElement = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'translation');
if (firstTranslationElement) {
DOMUtilities.insertBefore(newTu.asXmlElement(), firstTranslationElement);
inserted = true;
} else {
// no trans-unit, empty file, so add to bundle at end
translationbundleElem.appendChild(newTu.asXmlElement());
inserted = true;
}
} else {
const refUnitElement = DOMUtilities.getElementByTagNameAndId(this._parsedDocument, 'translation', 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 {
throw new Error(format('File "%s", xtb files are not translatable, they are already translations', filename));
}
}