From 339fdad74bbfcd74579f23fb7dfccbf1b6631b2c Mon Sep 17 00:00:00 2001 From: Martin Roob Date: Sat, 4 Aug 2018 12:28:04 +0200 Subject: [PATCH 1/4] ngx-i18nsupport #97 new serializer with beautifiy option --- Changelog.md | 9 + package.json | 1 - .../abstract-translation-messages-file.ts | 20 +- src/impl/xliff-file.spec.ts | 18 +- src/impl/xliff-file.ts | 9 + src/impl/xliff2-file.spec.ts | 10 + src/impl/xliff2-file.ts | 9 + src/impl/xmb-file.ts | 9 + src/impl/xml-serializer.spec.ts | 107 +++++++ src/impl/xml-serializer.ts | 279 ++++++++++++++++++ src/impl/xtb-file.ts | 9 + yarn.lock | 4 - 12 files changed, 467 insertions(+), 17 deletions(-) create mode 100644 src/impl/xml-serializer.spec.ts create mode 100644 src/impl/xml-serializer.ts diff --git a/Changelog.md b/Changelog.md index f167ac3..0089040 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,12 @@ + +# [1.9.3](https://github.com/martinroob/ngx-i18nsupport-lib/compare/v1.9.2...v1.9.3) (2018-08-03) + +### Bug Fixes + +* **beautifier** The pretty data beautifier caused some issues ([xliffmerge #97](https://github.com/martinroob/ngx-i18nsupport/issues/97)). +It is now replaced by an own implementation based on the serializer that is part of [xmldom](https://www.npmjs.com/package/xmldom). +This might result in slightly different formatted documents when using `beautifyOutput` + # [1.9.2](https://github.com/martinroob/ngx-i18nsupport-lib/compare/v1.9.2...v1.9.1) (2018-06-01) diff --git a/package.json b/package.json index d501e4c..437c982 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ }, "dependencies": { "@types/xmldom": "^0.1.29", - "pretty-data": "^0.40.0", "tokenizr": "^1.3.4", "xmldom": "^0.1.27" } diff --git a/src/impl/abstract-translation-messages-file.ts b/src/impl/abstract-translation-messages-file.ts index 0330493..f3fd46f 100644 --- a/src/impl/abstract-translation-messages-file.ts +++ b/src/impl/abstract-translation-messages-file.ts @@ -1,9 +1,7 @@ import {ITranslationMessagesFile, ITransUnit, STATE_NEW, STATE_TRANSLATED} from '../api'; import {isNullOrUndefined} from 'util'; -import {DOMParser, XMLSerializer} from 'xmldom'; -import * as prettyData from 'pretty-data'; -import {AbstractTransUnit} from './abstract-trans-unit'; -import {XtbFile} from './xtb-file'; +import {DOMParser} from 'xmldom'; +import {XmlSerializer, XmlSerializerOptions} from './xml-serializer'; /** * Created by roobm on 09.05.2017. * Abstract superclass for all implementations of ITranslationMessagesFile. @@ -58,6 +56,13 @@ export abstract class AbstractTranslationMessagesFile implements ITranslationMes abstract fileType(): string; + /** + * return tag names of all elements that have mixed content. + * These elements will not be beautified. + * Typical candidates are source and target. + */ + protected abstract elementsWithMixedContent(): string[]; + /** * Read all trans units from xml content. * Puts the found units into transUnits. @@ -272,10 +277,13 @@ export abstract class AbstractTranslationMessagesFile implements ITranslationMes * Default is false. */ public editedContent(beautifyOutput?: boolean): string { - let result = new XMLSerializer().serializeToString(this._parsedDocument); + let options: XmlSerializerOptions = {}; if (beautifyOutput === true) { - result = prettyData.pd.xml(result); + options.beautify = true; + options.indentString = ' '; + options.mixedContentElements = this.elementsWithMixedContent(); } + let result = new XmlSerializer().serializeToString(this._parsedDocument, options); if (this._fileEndsWithEOL) { // add eol if there was eol in original source return result + '\n'; diff --git a/src/impl/xliff-file.spec.ts b/src/impl/xliff-file.spec.ts index 5ac9dcf..888a1b1 100644 --- a/src/impl/xliff-file.spec.ts +++ b/src/impl/xliff-file.spec.ts @@ -57,15 +57,21 @@ describe('ngx-i18nsupport-lib xliff 1.2 test spec', () => { expect(tu.sourceContent()).toBe('Schließen'); }); - it('should read xlf file and pretty print it with pretty-data', () => { + it('should read xlf file and pretty print it', () => { const file: ITranslationMessagesFile = readFile(MASTER1SRC); expect(file).toBeTruthy(); expect(file.editedContent()).toContain('Beschreibung zu ()'); - expect(file.editedContent(true)).toContain(` Beschreibung zu - ( - ) - -`); + expect(file.editedContent(true)).toContain(` Beschreibung zu ()`); + }); + + it('should not add empty lines when beautifying (issue ngx-i18nsupport #97)', () => { + const file: ITranslationMessagesFile = readFile(MASTER1SRC); + expect(file).toBeTruthy(); + const editedContentBeautified = file.editedContent(true); + const file2: ITranslationMessagesFile = TranslationMessagesFileFactory.fromFileContent('xlf', editedContentBeautified, null, ENCODING); + const editedContentBeautifiedAgain = file2.editedContent(true); + expect(editedContentBeautifiedAgain).toMatch(/Beschreibung zu { diff --git a/src/impl/xliff-file.ts b/src/impl/xliff-file.ts index cdc5cb3..a5eaf95 100644 --- a/src/impl/xliff-file.ts +++ b/src/impl/xliff-file.ts @@ -60,6 +60,15 @@ export class XliffFile extends AbstractTranslationMessagesFile implements ITrans return FILETYPE_XLIFF12; } + /** + * 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 ['source', 'target', 'tool', 'seg-source', 'g', 'ph', 'bpt', 'ept', 'it', 'sub', 'mrk']; + } + protected initializeTransUnits() { this.transUnits = []; let transUnitsInFile = this._parsedDocument.getElementsByTagName('trans-unit'); diff --git a/src/impl/xliff2-file.spec.ts b/src/impl/xliff2-file.spec.ts index 551b921..adb2403 100644 --- a/src/impl/xliff2-file.spec.ts +++ b/src/impl/xliff2-file.spec.ts @@ -63,6 +63,16 @@ describe('ngx-i18nsupport-lib XLIFF 2.0 test spec', () => { expect(rereadFile.numberOfTransUnits()).toBe(file.numberOfTransUnits()); }); + it('should not add empty lines when beautifying (issue ngx-i18nsupport #97)', () => { + const file: ITranslationMessagesFile = readFile(TRANSLATED_FILE_SRC); + expect(file).toBeTruthy(); + const editedContentBeautified = file.editedContent(true); + const file2: ITranslationMessagesFile = TranslationMessagesFileFactory.fromUnknownFormatFileContent(editedContentBeautified, null, null); + const editedContentBeautifiedAgain = file2.editedContent(true); + expect(editedContentBeautifiedAgain).toMatch(/Diese Nachricht ist Diese Nachricht ist\s*\r\n?/); + }); + it('should emit warnings', () => { const file: ITranslationMessagesFile = readFile(MASTER1SRC); expect(file.warnings().length).toBe(1); diff --git a/src/impl/xliff2-file.ts b/src/impl/xliff2-file.ts index 6d4fc03..47f8427 100644 --- a/src/impl/xliff2-file.ts +++ b/src/impl/xliff2-file.ts @@ -62,6 +62,15 @@ export class Xliff2File extends AbstractTranslationMessagesFile implements ITran 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 = []; let transUnitsInFile = this._parsedDocument.getElementsByTagName('unit'); diff --git a/src/impl/xmb-file.ts b/src/impl/xmb-file.ts index 64da07f..c19bd36 100644 --- a/src/impl/xmb-file.ts +++ b/src/impl/xmb-file.ts @@ -64,6 +64,15 @@ export class XmbFile extends AbstractTranslationMessagesFile implements ITransla return FILETYPE_XMB; } + /** + * 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 ['message']; + } + /** * Guess language from filename. * If filename is foo.xy.xmb, than language is assumed to be xy. diff --git a/src/impl/xml-serializer.spec.ts b/src/impl/xml-serializer.spec.ts new file mode 100644 index 0000000..a68b779 --- /dev/null +++ b/src/impl/xml-serializer.spec.ts @@ -0,0 +1,107 @@ +/** + * Created by martin on 04.08.2018. + * Testcases for the XmlSerializer. + */ + +import {DOMParser} from 'xmldom'; +import {XmlSerializer, XmlSerializerOptions} from './xml-serializer'; +import {fail} from 'assert'; + +describe('XmlSerializer test spec', () => { + + let serializer: XmlSerializer; + + /** + * Helper. Parse an XML string. + * @param xmlstring + */ + function parseXmlString(xmlstring: string): Document { + return new DOMParser().parseFromString(xmlstring); + } + + beforeEach(() => { + serializer = new XmlSerializer(); + }); + + it("should serialize a simple document without any changes in output", () => { + let doc1string = `a test`; + const doc1: Document = parseXmlString(doc1string); + const serializedDoc = serializer.serializeToString(doc1); + expect(serializedDoc).toEqual(doc1string); + }); + + it("should serialize a complex document with attributes etc. without any changes in output", () => { + let doc1string = ` + + + +`; + const doc1: Document = parseXmlString(doc1string); + const serializedDoc = serializer.serializeToString(doc1); + expect(serializedDoc).toEqual(doc1string); + }); + + it("should beautify output using 2 spaces for indentation", () => { + let doc1string = ` +a simple pcdata element`; + const doc1: Document = parseXmlString(doc1string); + const beautifyOptions: XmlSerializerOptions = { + beautify: true + }; + const serializedDoc = serializer.serializeToString(doc1, beautifyOptions); + const expectedResult = ` + + a simple pcdata element +`; + expect(serializedDoc).toEqual(expectedResult); + }); + + it("should beautify output using e.g. tab for indentation", () => { + let doc1string = ` +a simple pcdata element`; + const doc1: Document = parseXmlString(doc1string); + const beautifyOptions: XmlSerializerOptions = { + beautify: true, + indentString: '\t' + }; + const serializedDoc = serializer.serializeToString(doc1, beautifyOptions); + const expectedResult = ` + +\ta simple pcdata element +`; + expect(serializedDoc).toEqual(expectedResult); + }); + + it("should throw an error if a non whitespace char is used for indentation", () => { + let doc1string = ` +a simple pcdata element`; + const doc1: Document = parseXmlString(doc1string); + const beautifyOptions: XmlSerializerOptions = { + beautify: true, + indentString: '\tx' + }; + try { + serializer.serializeToString(doc1, beautifyOptions); + fail('oops, error expected here'); + } catch (err) { + expect(err.message).toBe('indentString must not contain non white characters'); + } + }); + + it("should beautify output with mixed content", () => { + let doc1string = ` +a mixed content element`; + const doc1: Document = parseXmlString(doc1string); + const beautifyOptions: XmlSerializerOptions = { + beautify: true, + mixedContentElements: ['y'] + }; + const serializedDoc = serializer.serializeToString(doc1, beautifyOptions); + const expectedResult = ` + + a mixed content element +`; + expect(serializedDoc).toEqual(expectedResult); + }); + +}); diff --git a/src/impl/xml-serializer.ts b/src/impl/xml-serializer.ts new file mode 100644 index 0000000..2d3caa8 --- /dev/null +++ b/src/impl/xml-serializer.ts @@ -0,0 +1,279 @@ +/** + * An XmlSerializer that supports formatting. + * Original code is based on [xmldom](https://www.npmjs.com/package/xmldom) + * It is extended to support formatting including handling of elements with mixed content. + * Example formatted output: + *
+ *     
+ *         An element with
+ *             mixed
+ *              content
+ *         
+ *     
+ * 
+ * Same when "element" is indicated as "mixedContentElement": + *
+ *     
+ *         An element with mixed content
+ *     
+ * 
+ */ + +interface Namespace { + prefix: string; + namespace: string; +} + +/** + * Options used to control the formatting + */ +export interface XmlSerializerOptions { + beautify?: boolean; // set to activate beautify + indentString?: string; // Sequence uses for indentation, must only contain white space chars, e.g. " " or " " or "\t" + mixedContentElements?: string[]; // Names of elements containing mixed content (these are not beautified) +} + +const DEFAULT_INDENT_STRING = ' '; + +export class XmlSerializer { + + constructor() { + + } + + /** + * Serialze xml document to string. + * @param document the document + * @param options can be used to activate beautifying. + */ + serializeToString(document: Document, options?: XmlSerializerOptions): string { + let buf = []; + let visibleNamespaces: Namespace[] = []; + const refNode = document.documentElement; + let prefix = refNode.prefix; + const uri = refNode.namespaceURI; + + if (uri && prefix == null) { + prefix = refNode.lookupPrefix(uri); + if (prefix == null) { + visibleNamespaces = [ + {namespace: uri, prefix: null} + //{namespace:uri,prefix:''} + ] + } + } + if (!options) { + options = {}; + } + if (options.indentString) { + if (!this.containsOnlyWhiteSpace(options.indentString)) { + throw new Error('indentString must not contain non white characters'); + } + } + this.doSerializeToString(document, options, buf, 0, false, visibleNamespaces); + return buf.join(''); + } + + /** + * Main format method that does all the work. + * Outputs a node to the outputbuffer. + * @param node the node to be formatted. + * @param options + * @param buf outputbuffer, new output will be appended to this array. + * @param indentLevel Lever of indentation for formatted output. + * @param partOfMixedContent true, if node is a subelement of an element containind mixed content. + * @param visibleNamespaces + */ + private doSerializeToString(node: Node, options: XmlSerializerOptions, buf: string[], indentLevel: number, partOfMixedContent: boolean, visibleNamespaces: Namespace[]) { + let child: Node; + switch (node.nodeType) { + case node.ELEMENT_NODE: + const elementNode: Element = node; + const attrs = elementNode.attributes; + const len = attrs.length; + child = elementNode.firstChild; + const nodeName = elementNode.tagName; + const elementHasMixedContent = this.isMixedContentElement(nodeName, options); + if (partOfMixedContent) { + buf.push('<' , nodeName); + } else { + this.outputIndented(options, buf, indentLevel, '<' , nodeName); + } + + for (let i = 0; i < len; i++) { + // add namespaces for attributes + let attr = attrs.item(i); + if (attr.prefix === 'xmlns') { + visibleNamespaces.push({prefix: attr.localName, namespace: attr.value}); + } else if (attr.nodeName === 'xmlns') { + visibleNamespaces.push({prefix: '', namespace: attr.value}); + } + } + for (let i = 0; i < len; i++) { + let attr = attrs.item(i); + if (this.needNamespaceDefine(attr, visibleNamespaces)) { + const prefix = attr.prefix || ''; + const uri = attr.namespaceURI; + const ns = prefix ? ' xmlns:' + prefix : " xmlns"; + buf.push(ns, '="', uri, '"'); + visibleNamespaces.push({prefix: prefix, namespace: uri}); + } + this.doSerializeToString(attr, options, buf, indentLevel, false, visibleNamespaces); + } + // add namespace for current node + if (this.needNamespaceDefine(elementNode, visibleNamespaces)) { + const prefix = elementNode.prefix || ''; + const uri = node.namespaceURI; + const ns = prefix ? ' xmlns:' + prefix : " xmlns"; + buf.push(ns, '="', uri, '"'); + visibleNamespaces.push({prefix: prefix, namespace: uri}); + } + + if (child) { + buf.push('>'); + //if is cdata child node + let hasComplexContent = false; + while (child) { + if (child.nodeType === child.ELEMENT_NODE) { + hasComplexContent = true; + } + this.doSerializeToString(child, options, buf, indentLevel + 1, partOfMixedContent || elementHasMixedContent, visibleNamespaces); + child = child.nextSibling; + } + if (!partOfMixedContent && !elementHasMixedContent && hasComplexContent) { + this.outputIndented(options, buf, indentLevel, ''); + } else { + buf.push(''); + } + } else { + buf.push('/>'); + } + return; + case node.DOCUMENT_NODE: + case node.DOCUMENT_FRAGMENT_NODE: + child = node.firstChild; + while (child) { + this.doSerializeToString(child, options, buf, indentLevel, false, visibleNamespaces); + child = child.nextSibling; + } + return; + case node.ATTRIBUTE_NODE: + const attrNode = node; + return buf.push(' ', attrNode.name, '="', attrNode.value.replace(/[<&"]/g, this._xmlEncoder), '"'); + case node.TEXT_NODE: + const textNode = node; + if (!options.beautify || partOfMixedContent || !this.containsOnlyWhiteSpace(textNode.data)) { + return buf.push(textNode.data.replace(/[<&]/g, this._xmlEncoder)); + } + return; + case node.CDATA_SECTION_NODE: + const cdatasectionNode = node; + return buf.push(''); + case node.COMMENT_NODE: + const commentNode = node; + return buf.push(""); + case node.DOCUMENT_TYPE_NODE: + const documenttypeNode = node; + const pubid = documenttypeNode.publicId; + const sysid = documenttypeNode.systemId; + buf.push(''); + } else if (sysid && sysid !== '.') { + buf.push(' SYSTEM "', sysid, '">'); + } else { + var sub = documenttypeNode.internalSubset; + if (sub) { + buf.push(" [", sub, "]"); + } + buf.push(">"); + } + return; + case node.PROCESSING_INSTRUCTION_NODE: + const piNode = node; + return buf.push(""); + case node.ENTITY_REFERENCE_NODE: + return buf.push('&', node.nodeName, ';'); + //case ENTITY_NODE: + //case NOTATION_NODE: + default: + buf.push('??', node.nodeName); + } + } + + private needNamespaceDefine(node: Element | Attr, visibleNamespaces: Namespace[]): boolean { + const prefix = node.prefix || ''; + const uri = node.namespaceURI; + if (!prefix && !uri) { + return false; + } + if (prefix === "xml" && uri === "http://www.w3.org/XML/1998/namespace" + || uri === 'http://www.w3.org/2000/xmlns/') { + return false; + } + + let i = visibleNamespaces.length; + while (i--) { + const ns = visibleNamespaces[i]; + // get namespace prefix + if (ns.prefix === prefix) { + return ns.namespace !== uri; + } + } + return true; + } + + private _xmlEncoder(c: string): string { + return c === '<' && '<' || + c === '>' && '>' || + c === '&' && '&' || + c === '"' && '"' || + '&#'+ c.charCodeAt(0)+';' + } + + private outputIndented(options: XmlSerializerOptions, buf: string[], indentLevel: number, ...outputParts: string[]) { + if (options.beautify) { + buf.push('\n'); + if (indentLevel > 0) { + buf.push(this.indentationString(options, indentLevel)); + } + } + buf.push(...outputParts); + } + + private indentationString(options: XmlSerializerOptions, indentLevel: number): string { + const indent = (options.indentString) ? options.indentString : DEFAULT_INDENT_STRING; + let result = ''; + for (let i = 0; i < indentLevel; i++) { + result = result + indent; + } + return result; + } + + /** + * Test, wether tagName is an element containing mixed content. + * @param tagName + * @param options + */ + private isMixedContentElement(tagName: string, options: XmlSerializerOptions): boolean { + if (options && options.mixedContentElements) { + return !!options.mixedContentElements.find((tag) => tag === tagName); + } else { + return false; + } + } + + private containsOnlyWhiteSpace(text: string): boolean { + for (let i = 0; i < text.length; i++) { + const c = text.charAt(i); + if (!(c === ' ' || c === '\t' || c === '\r' || c === '\n')) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/src/impl/xtb-file.ts b/src/impl/xtb-file.ts index e5149e1..a2a57a3 100644 --- a/src/impl/xtb-file.ts +++ b/src/impl/xtb-file.ts @@ -99,6 +99,15 @@ export class XtbFile extends AbstractTranslationMessagesFile implements ITransla 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. diff --git a/yarn.lock b/yarn.lock index 5285324..7361087 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3849,10 +3849,6 @@ pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" -pretty-data@^0.40.0: - version "0.40.0" - resolved "https://registry.yarnpkg.com/pretty-data/-/pretty-data-0.40.0.tgz#572aa8ea23467467ab94b6b5266a6fd9c8fddd72" - private@^0.1.6, private@^0.1.8, private@~0.1.5: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" From bb973112ee32fdf13aca097ef4758d09f1114131 Mon Sep 17 00:00:00 2001 From: Martin Roob Date: Sun, 5 Aug 2018 18:07:19 +0200 Subject: [PATCH 2/4] prepare for 1.10.0 --- Changelog.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Changelog.md b/Changelog.md index 0089040..25838cd 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,12 +1,19 @@ - -# [1.9.3](https://github.com/martinroob/ngx-i18nsupport-lib/compare/v1.9.2...v1.9.3) (2018-08-03) + +# [1.10.0](https://github.com/martinroob/ngx-i18nsupport-lib/compare/v1.9.2...v1.10.0) (2018-08-05) ### Bug Fixes -* **beautifier** The pretty data beautifier caused some issues ([xliffmerge #97](https://github.com/martinroob/ngx-i18nsupport/issues/97)). +* **beautifier** [#52](https://github.com/martinroob/ngx-i18nsupport-lib/issues/52) +The pretty data beautifier caused some issues ([xliffmerge #97](https://github.com/martinroob/ngx-i18nsupport/issues/97)). It is now replaced by an own implementation based on the serializer that is part of [xmldom](https://www.npmjs.com/package/xmldom). This might result in slightly different formatted documents when using `beautifyOutput` +### Features + +* **API** [#53](https://github.com/martinroob/ngx-i18nsupport-lib/issues/53) +`importNewTransunit` now allows to specify the ancestor of the newly imported unit. There is an optional new parameter for this, so the change is not breaking. +If you do not use this parametes, the behaviour is the same as before (adding at the end). + # [1.9.2](https://github.com/martinroob/ngx-i18nsupport-lib/compare/v1.9.2...v1.9.1) (2018-06-01) From 368997ca93c786a256bcd5a551ec6a2740fd345a Mon Sep 17 00:00:00 2001 From: Martin Roob Date: Sun, 5 Aug 2018 18:08:34 +0200 Subject: [PATCH 3/4] closes #53: importNewTransunit now allows to specify the ancestor of the newly imported unit --- src/api/i-translation-messages-file.ts | 6 +- .../abstract-translation-messages-file.ts | 9 ++- src/impl/dom-utilities.ts | 77 ++++++++++++++++++- src/impl/xliff-file.spec.ts | 40 ++++++++++ src/impl/xliff-file.ts | 51 ++++++++++-- src/impl/xliff2-file.spec.ts | 41 ++++++++++ src/impl/xliff2-file.ts | 51 +++++++++--- src/impl/xmb-file.ts | 8 +- src/impl/xtb-file.spec.ts | 42 ++++++++++ src/impl/xtb-file.ts | 67 ++++++++++++---- 10 files changed, 352 insertions(+), 40 deletions(-) diff --git a/src/api/i-translation-messages-file.ts b/src/api/i-translation-messages-file.ts index c16910c..d93b242 100644 --- a/src/api/i-translation-messages-file.ts +++ b/src/api/i-translation-messages-file.ts @@ -136,10 +136,14 @@ export interface ITranslationMessagesFile { * @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): ITransUnit; + importNewTransUnit(foreignTransUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean, importAfterElement?: ITransUnit): ITransUnit; /** * Remove the trans-unit with the given id. diff --git a/src/impl/abstract-translation-messages-file.ts b/src/impl/abstract-translation-messages-file.ts index f3fd46f..4b8a782 100644 --- a/src/impl/abstract-translation-messages-file.ts +++ b/src/impl/abstract-translation-messages-file.ts @@ -229,7 +229,7 @@ export abstract class AbstractTranslationMessagesFile implements ITranslationMes * depending on the values of isDefaultLang and copyContent. * So the source can be used as a dummy translation. * (used by xliffmerge) - * @param transUnit the trans unit to be imported. + * @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. @@ -237,9 +237,14 @@ export abstract class AbstractTranslationMessagesFile implements ITranslationMes * @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. */ - abstract importNewTransUnit(transUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean); + abstract importNewTransUnit(foreignTransUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean, importAfterElement?: ITransUnit): ITransUnit; /** * Remove the trans-unit with the given id. diff --git a/src/impl/dom-utilities.ts b/src/impl/dom-utilities.ts index 6b0e6d4..bf53a97 100644 --- a/src/impl/dom-utilities.ts +++ b/src/impl/dom-utilities.ts @@ -21,6 +21,62 @@ export class DOMUtilities { } } + /** + * return an element with the given tag and id attribute. + * @param element + * @param tagName + * @param id + * @return {Element} subelement or null, if not existing. + */ + public static getElementByTagNameAndId(element: Element | Document, tagName: string, id: string): Element { + let matchingElements = element.getElementsByTagName(tagName); + if (matchingElements && matchingElements.length > 0) { + for (let i = 0; i < matchingElements.length; i++) { + const node: Element = matchingElements.item(i); + if (node.getAttribute('id') === id) { + return node; + } + } + } + return null; + } + + /** + * Get next sibling, that is an element. + * @param element + */ + public static getElementFollowingSibling(element: Element): Element { + if (!element) { + return null; + } + let e = element.nextSibling; + while (e) { + if (e.nodeType === e.ELEMENT_NODE) { + return e; + } + e = e.nextSibling; + } + return null; + } + + /** + * Get previous sibling, that is an element. + * @param element + */ + public static getElementPrecedingSibling(element: Element): Element { + if (!element) { + return null; + } + let e = element.previousSibling; + while (e) { + if (e.nodeType === e.ELEMENT_NODE) { + return e; + } + e = e.previousSibling; + } + return null; + } + /** * return content of element as string, including all markup. * @param element @@ -101,7 +157,16 @@ export class DOMUtilities { * @return {Element} */ public static createFollowingSibling(elementNameToCreate: string, previousSibling: Node): Element { - const newElement = previousSibling.ownerDocument.createElement('target'); + const newElement = previousSibling.ownerDocument.createElement(elementNameToCreate); + return DOMUtilities.insertAfter(newElement, previousSibling); + } + + /** + * Insert newElement directly after previousSibling. + * @param newElement + * @param previousSibling + */ + public static insertAfter(newElement: Node, previousSibling: Node): Node { if (previousSibling.nextSibling !== null) { previousSibling.parentNode.insertBefore(newElement, previousSibling.nextSibling); } else { @@ -109,4 +174,14 @@ export class DOMUtilities { } return newElement; } + + /** + * Insert newElement directly before nextSibling. + * @param newElement + * @param nextSibling + */ + public static insertBefore(newElement: Node, nextSibling: Node): Node { + nextSibling.parentNode.insertBefore(newElement, nextSibling); + return newElement; + } } \ No newline at end of file diff --git a/src/impl/xliff-file.spec.ts b/src/impl/xliff-file.spec.ts index 888a1b1..51bed54 100644 --- a/src/impl/xliff-file.spec.ts +++ b/src/impl/xliff-file.spec.ts @@ -475,6 +475,46 @@ describe('ngx-i18nsupport-lib xliff 1.2 test spec', () => { expect(targetTu.sourceContent()).toBe('Test for merging units'); }); + it ('should copy a transunit to a specified position (#53)', () => { + const file: ITranslationMessagesFile = readFile(MASTER1SRC); + const tu: ITransUnit = file.transUnitWithId(ID_TO_MERGE); + expect(tu).toBeTruthy(); + const targetFile: ITranslationMessagesFile = readFile(TRANSLATED_FILE_SRC); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeFalsy(); + const ID_EXISTING = 'f540f05dc71be88e226a3920dbf1140b2658e5ea'; + const existingTu = targetFile.transUnitWithId(ID_EXISTING); + expect(existingTu).toBeTruthy(); + const newTu = targetFile.importNewTransUnit(tu, false, true, existingTu); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeTruthy(); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toEqual(newTu); + const doc: Document = new DOMParser().parseFromString(targetFile.editedContent()); + const existingElem = DOMUtilities.getElementByTagNameAndId(doc, 'trans-unit', ID_EXISTING); + const newElem = DOMUtilities.getElementByTagNameAndId(doc, 'trans-unit', ID_TO_MERGE); + expect(DOMUtilities.getElementFollowingSibling(existingElem)).toEqual(newElem); + let changedTargetFile = TranslationMessagesFileFactory.fromUnknownFormatFileContent(targetFile.editedContent(), null, null); + let targetTu = changedTargetFile.transUnitWithId(ID_TO_MERGE); + expect(targetTu.sourceContent()).toBe('Test for merging units'); + }); + + it ('should copy a transunit to first position (#53)', () => { + const file: ITranslationMessagesFile = readFile(MASTER1SRC); + const tu: ITransUnit = file.transUnitWithId(ID_TO_MERGE); + expect(tu).toBeTruthy(); + const targetFile: ITranslationMessagesFile = readFile(TRANSLATED_FILE_SRC); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeFalsy(); + // when importNewTransUnit is called with null, new unit will be added at first position + const newTu = targetFile.importNewTransUnit(tu, false, true, null); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeTruthy(); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toEqual(newTu); + const doc: Document = new DOMParser().parseFromString(targetFile.editedContent()); + const newElem = DOMUtilities.getElementByTagNameAndId(doc, 'trans-unit', ID_TO_MERGE); + expect(newElem).toBeTruthy(); + expect(DOMUtilities.getElementPrecedingSibling(newElem)).toBeFalsy(); + let changedTargetFile = TranslationMessagesFileFactory.fromUnknownFormatFileContent(targetFile.editedContent(), null, null); + let targetTu = changedTargetFile.transUnitWithId(ID_TO_MERGE); + expect(targetTu.sourceContent()).toBe('Test for merging units'); + }); + it ('should copy source to target and set a praefix and suffix', () => { const file: ITranslationMessagesFile = readFile(MASTER1SRC); file.setNewTransUnitTargetPraefix('%%'); diff --git a/src/impl/xliff-file.ts b/src/impl/xliff-file.ts index a5eaf95..ee4cb0d 100644 --- a/src/impl/xliff-file.ts +++ b/src/impl/xliff-file.ts @@ -1,4 +1,3 @@ -import {DOMParser} from "xmldom"; import {format} from 'util'; import {ITranslationMessagesFile, ITransUnit, FORMAT_XLIFF12, FILETYPE_XLIFF12} from '../api'; import {DOMUtilities} from './dom-utilities'; @@ -137,7 +136,7 @@ export class XliffFile extends AbstractTranslationMessagesFile implements ITrans * depending on the values of isDefaultLang and copyContent. * So the source can be used as a dummy translation. * (used by xliffmerge) - * @param transUnit the trans unit to be imported. + * @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. @@ -145,22 +144,58 @@ export class XliffFile extends AbstractTranslationMessagesFile implements ITrans * @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. */ - public importNewTransUnit(transUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean): ITransUnit { - if (this.transUnitWithId(transUnit.id)) { - throw new Error(format('tu with id %s already exists in file, cannot import it', transUnit.id)); + 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)); } - let newTu = ( transUnit).cloneWithSourceAsTarget(isDefaultLang, copyContent, this); + let newTu = ( foreignTransUnit).cloneWithSourceAsTarget(isDefaultLang, copyContent, this); let bodyElement = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'body'); - if (bodyElement) { + if (!bodyElement) { + throw new Error(format('File "%s" seems to be no xliff 1.2 file (should contain a body element)', this._filename)); + } + let inserted = false; + let isAfterElementPartOfFile = false; + if (!!importAfterElement) { + let insertionPoint = this.transUnitWithId(importAfterElement.id); + if (!!insertionPoint) { + isAfterElementPartOfFile = true; + } + } + if (importAfterElement === undefined || (importAfterElement && !isAfterElementPartOfFile)) { bodyElement.appendChild(newTu.asXmlElement()); + inserted = true; + } else if (importAfterElement === null) { + let firstUnitElement = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'trans-unit'); + if (firstUnitElement) { + DOMUtilities.insertBefore(newTu.asXmlElement(), firstUnitElement); + inserted = true; + } else { + // no trans-unit, empty file, so add to body + bodyElement.appendChild(newTu.asXmlElement()); + inserted = true; + } + } else { + let refUnitElement = DOMUtilities.getElementByTagNameAndId(this._parsedDocument, 'trans-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; } - return newTu; } /** diff --git a/src/impl/xliff2-file.spec.ts b/src/impl/xliff2-file.spec.ts index adb2403..4ba2a64 100644 --- a/src/impl/xliff2-file.spec.ts +++ b/src/impl/xliff2-file.spec.ts @@ -2,6 +2,7 @@ import {TranslationMessagesFileFactory, ITranslationMessagesFile, ITransUnit, IN import * as fs from "fs"; import {AbstractTransUnit} from './abstract-trans-unit'; import {DOMUtilities} from './dom-utilities'; +import {DOMParser} from 'xmldom'; /** * Created by martin on 05.05.2017. @@ -444,6 +445,46 @@ describe('ngx-i18nsupport-lib XLIFF 2.0 test spec', () => { expect(targetTu.sourceContent()).toBe('Test for merging units'); }); + it ('should copy a transunit to a specified position (#53)', () => { + const file: ITranslationMessagesFile = readFile(MASTER1SRC); + const tu: ITransUnit = file.transUnitWithId(ID_TO_MERGE); + expect(tu).toBeTruthy(); + const targetFile: ITranslationMessagesFile = readFile(TRANSLATED_FILE_SRC); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeFalsy(); + const ID_EXISTING = '7499557905529977371'; + const existingTu = targetFile.transUnitWithId(ID_EXISTING); + expect(existingTu).toBeTruthy(); + const newTu = targetFile.importNewTransUnit(tu, false, true, existingTu); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeTruthy(); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toEqual(newTu); + const doc: Document = new DOMParser().parseFromString(targetFile.editedContent()); + const existingElem = DOMUtilities.getElementByTagNameAndId(doc, 'unit', ID_EXISTING); + const newElem = DOMUtilities.getElementByTagNameAndId(doc, 'unit', ID_TO_MERGE); + expect(DOMUtilities.getElementFollowingSibling(existingElem)).toEqual(newElem); + let changedTargetFile = TranslationMessagesFileFactory.fromUnknownFormatFileContent(targetFile.editedContent(), null, null); + let targetTu = changedTargetFile.transUnitWithId(ID_TO_MERGE); + expect(targetTu.sourceContent()).toBe('Test for merging units'); + }); + + it ('should copy a transunit to first position (#53)', () => { + const file: ITranslationMessagesFile = readFile(MASTER1SRC); + const tu: ITransUnit = file.transUnitWithId(ID_TO_MERGE); + expect(tu).toBeTruthy(); + const targetFile: ITranslationMessagesFile = readFile(TRANSLATED_FILE_SRC); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeFalsy(); + // when importNewTransUnit is called with null, new unit will be added at first position + const newTu = targetFile.importNewTransUnit(tu, false, true, null); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeTruthy(); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toEqual(newTu); + const doc: Document = new DOMParser().parseFromString(targetFile.editedContent()); + const newElem = DOMUtilities.getElementByTagNameAndId(doc, 'unit', ID_TO_MERGE); + expect(newElem).toBeTruthy(); + expect(DOMUtilities.getElementPrecedingSibling(newElem)).toBeFalsy(); + let changedTargetFile = TranslationMessagesFileFactory.fromUnknownFormatFileContent(targetFile.editedContent(), null, null); + let targetTu = changedTargetFile.transUnitWithId(ID_TO_MERGE); + expect(targetTu.sourceContent()).toBe('Test for merging units'); + }); + it ('should copy source to target and set a praefix and suffix', () => { const file: ITranslationMessagesFile = readFile(MASTER1SRC); file.setNewTransUnitTargetPraefix('%%'); diff --git a/src/impl/xliff2-file.ts b/src/impl/xliff2-file.ts index 47f8427..19152ed 100644 --- a/src/impl/xliff2-file.ts +++ b/src/impl/xliff2-file.ts @@ -1,4 +1,3 @@ -import {DOMParser} from "xmldom"; import {format} from 'util'; import {ITranslationMessagesFile, ITransUnit, FORMAT_XLIFF20, FILETYPE_XLIFF20} from '../api'; import {DOMUtilities} from './dom-utilities'; @@ -139,7 +138,7 @@ export class Xliff2File extends AbstractTranslationMessagesFile implements ITran * depending on the values of isDefaultLang and copyContent. * So the source can be used as a dummy translation. * (used by xliffmerge) - * @param transUnit the trans unit to be imported. + * @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. @@ -147,24 +146,58 @@ export class Xliff2File extends AbstractTranslationMessagesFile implements ITran * @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. */ - public importNewTransUnit(transUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean): ITransUnit { - if (this.transUnitWithId(transUnit.id)) { - throw new Error(format('tu with id %s already exists in file, cannot import it', transUnit.id)); + 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)); } - let newTu = ( transUnit).cloneWithSourceAsTarget(isDefaultLang, copyContent, this); + let newTu = ( foreignTransUnit).cloneWithSourceAsTarget(isDefaultLang, copyContent, this); let fileElement = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'file'); - if (fileElement) { + if (!fileElement) { + throw new Error(format('File "%s" seems to be no xliff 2.0 file (should contain a file element)', this._filename)); + } + let inserted = false; + let isAfterElementPartOfFile = false; + if (!!importAfterElement) { + let insertionPoint = this.transUnitWithId(importAfterElement.id); + if (!!insertionPoint) { + isAfterElementPartOfFile = true; + } + } + if (importAfterElement === undefined || (importAfterElement && !isAfterElementPartOfFile)) { fileElement.appendChild(newTu.asXmlElement()); + inserted = true; + } else if (importAfterElement === null) { + let 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 { + let 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 { - throw new Error(format('File "%s" seems to be no xliff 2.0 file (should contain a file element)', this._filename)); + return null; } - return newTu; } /** diff --git a/src/impl/xmb-file.ts b/src/impl/xmb-file.ts index c19bd36..2397bcc 100644 --- a/src/impl/xmb-file.ts +++ b/src/impl/xmb-file.ts @@ -133,7 +133,7 @@ export class XmbFile extends AbstractTranslationMessagesFile implements ITransla * depending on the values of isDefaultLang and copyContent. * So the source can be used as a dummy translation. * (used by xliffmerge) - * @param transUnit the trans unit to be imported. + * @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. @@ -141,10 +141,14 @@ export class XmbFile extends AbstractTranslationMessagesFile implements ITransla * @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. */ - public importNewTransUnit(transUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean): ITransUnit { + importNewTransUnit(foreignTransUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean, importAfterElement?: ITransUnit): ITransUnit { throw Error('xmb file cannot be used to store translations, use xtb file'); } diff --git a/src/impl/xtb-file.spec.ts b/src/impl/xtb-file.spec.ts index f2a8cf4..44c69db 100644 --- a/src/impl/xtb-file.spec.ts +++ b/src/impl/xtb-file.spec.ts @@ -1,5 +1,7 @@ import {TranslationMessagesFileFactory, ITranslationMessagesFile, ITransUnit, INormalizedMessage, STATE_NEW, STATE_TRANSLATED, STATE_FINAL} from '../api'; import * as fs from "fs"; +import {DOMParser} from 'xmldom'; +import {DOMUtilities} from './dom-utilities'; /** * Created by martin on 28.04.2017. @@ -314,6 +316,46 @@ describe('ngx-i18nsupport-lib xtb test spec', () => { expect(targetTu.targetContent()).toBe('Test for merging units'); }); + it ('should copy a transunit to a specified position (#53)', () => { + const file: ITranslationMessagesFile = readUnknownFormatFile(MASTER_1_XMB); + const tu: ITransUnit = file.transUnitWithId(ID_TO_MERGE); + expect(tu).toBeTruthy(); + const targetFile: ITranslationMessagesFile = readFile(TRANSLATION_EN_XTB, MASTER_1_XMB); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeFalsy(); + const ID_EXISTING = '4371668001355139802'; + const existingTu = targetFile.transUnitWithId(ID_EXISTING); + expect(existingTu).toBeTruthy(); + const newTu = targetFile.importNewTransUnit(tu, false, true, existingTu); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeTruthy(); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toEqual(newTu); + const doc: Document = new DOMParser().parseFromString(targetFile.editedContent()); + const existingElem = DOMUtilities.getElementByTagNameAndId(doc, 'translation', ID_EXISTING); + const newElem = DOMUtilities.getElementByTagNameAndId(doc, 'translation', ID_TO_MERGE); + expect(DOMUtilities.getElementFollowingSibling(existingElem)).toEqual(newElem); + let changedTargetFile = TranslationMessagesFileFactory.fromUnknownFormatFileContent(targetFile.editedContent(), null, null); + let targetTu = changedTargetFile.transUnitWithId(ID_TO_MERGE); + expect(targetTu.targetContent()).toBe('Test for merging units'); + }); + + it ('should copy a transunit to first position (#53)', () => { + const file: ITranslationMessagesFile = readUnknownFormatFile(MASTER_1_XMB); + const tu: ITransUnit = file.transUnitWithId(ID_TO_MERGE); + expect(tu).toBeTruthy(); + const targetFile: ITranslationMessagesFile = readFile(TRANSLATION_EN_XTB, MASTER_1_XMB); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeFalsy(); + // when importNewTransUnit is called with null, new unit will be added at first position + const newTu = targetFile.importNewTransUnit(tu, false, true, null); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toBeTruthy(); + expect(targetFile.transUnitWithId(ID_TO_MERGE)).toEqual(newTu); + const doc: Document = new DOMParser().parseFromString(targetFile.editedContent()); + const newElem = DOMUtilities.getElementByTagNameAndId(doc, 'translation', ID_TO_MERGE); + expect(newElem).toBeTruthy(); + expect(DOMUtilities.getElementPrecedingSibling(newElem)).toBeFalsy(); + let changedTargetFile = TranslationMessagesFileFactory.fromUnknownFormatFileContent(targetFile.editedContent(), null, null); + let targetTu = changedTargetFile.transUnitWithId(ID_TO_MERGE); + expect(targetTu.targetContent()).toBe('Test for merging units'); + }); + it ('should copy a transunit from file a to file b and set a praefix and suffix', () => { const file: ITranslationMessagesFile = readUnknownFormatFile(MASTER_1_XMB); const tu: ITransUnit = file.transUnitWithId(ID_TO_MERGE); diff --git a/src/impl/xtb-file.ts b/src/impl/xtb-file.ts index a2a57a3..34994c2 100644 --- a/src/impl/xtb-file.ts +++ b/src/impl/xtb-file.ts @@ -1,4 +1,3 @@ -import {DOMParser} from "xmldom"; import {ITranslationMessagesFile, ITransUnit, FILETYPE_XTB, FORMAT_XTB} from '../api'; import {format} from 'util'; import {DOMUtilities} from './dom-utilities'; @@ -162,7 +161,7 @@ export class XtbFile extends AbstractTranslationMessagesFile implements ITransla * depending on the values of isDefaultLang and copyContent. * So the source can be used as a dummy translation. * (used by xliffmerge) - * @param transUnit the trans unit to be imported. + * @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. @@ -170,29 +169,63 @@ export class XtbFile extends AbstractTranslationMessagesFile implements ITransla * @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. */ - public importNewTransUnit(transUnit: ITransUnit, isDefaultLang: boolean, copyContent: boolean): ITransUnit { - if (this.transUnitWithId(transUnit.id)) { - throw new Error(format('tu with id %s already exists in file, cannot import it', transUnit.id)); + 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)); } - let newTu = ( transUnit).cloneWithSourceAsTarget(isDefaultLang, copyContent, this); + let newMasterTu = ( foreignTransUnit).cloneWithSourceAsTarget(isDefaultLang, copyContent, this); let translationbundleElem = DOMUtilities.getFirstElementByTagName(this._parsedDocument, 'translationbundle'); - if (translationbundleElem) { - let translationElement = translationbundleElem.ownerDocument.createElement('translation'); - translationElement.setAttribute('id', transUnit.id); - let newContent = transUnit.sourceContent(); - if (!( transUnit).isICUMessage(newContent)) { - newContent = this.getNewTransUnitTargetPraefix() + newContent + this.getNewTransUnitTargetSuffix(); + if (!translationbundleElem) { + throw new Error(format('File "%s" seems to be no xtb file (should contain a translationbundle element)', this._filename)); + } + let translationElement = translationbundleElem.ownerDocument.createElement('translation'); + translationElement.setAttribute('id', foreignTransUnit.id); + let newContent = foreignTransUnit.sourceContent(); + if (!( foreignTransUnit).isICUMessage(newContent)) { + newContent = this.getNewTransUnitTargetPraefix() + newContent + this.getNewTransUnitTargetSuffix(); + } + DOMUtilities.replaceContentWithXMLContent(translationElement, newContent); + let newTu = new XtbTransUnit(translationElement, foreignTransUnit.id, this, newMasterTu); + let inserted = false; + let isAfterElementPartOfFile = false; + if (!!importAfterElement) { + let insertionPoint = this.transUnitWithId(importAfterElement.id); + if (!!insertionPoint) { + isAfterElementPartOfFile = true; + } + } + if (importAfterElement === undefined || (importAfterElement && !isAfterElementPartOfFile)) { + translationbundleElem.appendChild(newTu.asXmlElement()); + inserted = true; + } else if (importAfterElement === null) { + let 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 { + let refUnitElement = DOMUtilities.getElementByTagNameAndId(this._parsedDocument, 'translation', importAfterElement.id); + if (refUnitElement) { + DOMUtilities.insertAfter(newTu.asXmlElement(), refUnitElement); + inserted = true; } - DOMUtilities.replaceContentWithXMLContent(translationElement, newContent); - let newTransUnit = new XtbTransUnit(translationElement, transUnit.id, this, newTu); - translationbundleElem.appendChild(translationElement); + } + if (inserted) { this.lazyInitializeTransUnits(); - this.transUnits.push(newTransUnit); + this.transUnits.push(newTu); this.countNumbers(); - return newTransUnit; + return newTu; } else { return null; } From af783a8037e07739800cab674352152b7b991a0f Mon Sep 17 00:00:00 2001 From: Martin Roob Date: Sun, 5 Aug 2018 18:09:57 +0200 Subject: [PATCH 4/4] v1.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 437c982..502d3a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ngx-i18nsupport-lib", - "version": "1.9.2", + "version": "1.10.0", "description": "A Typescript library to work with Angular generated i18n files (xliff, xmb)", "main": "bundles/ngx-i18nsupport-lib.umd.js", "module": "./dist/index.js",