Skip to content

Commit

Permalink
Merge pull request #107 from daniel-sc/106-keep-additional-attributes
Browse files Browse the repository at this point in the history
fix: keep unknown xml attributes
  • Loading branch information
daniel-sc committed Mar 21, 2024
2 parents 94e1fad + e34c014 commit f5f837d
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 3 deletions.
23 changes: 23 additions & 0 deletions src/merger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,28 @@ describe('merger', () => {
{id: 'c', source: 'ccccccc', target: 'ccc1', state: 'translated', locations: [], description: undefined, meaning: undefined},
]);
});

it('should keep additionalAttribute of target', () => {
const translationSourceFile = new TranslationFile([
{id: 'a1', source: 'aaaaaaaaaaa1', locations: [], additionalAttributes: [{name: 'a', value: 'b', path: '.'}]},
{id: 'a2', source: 'aaaaaaaaaaa2', locations: []},
{id: 'b', source: 'bbbbbbb', locations: []},
{id: 'c', source: 'ccccccc', locations: []},
], 'en');
const translationTargetFile = new TranslationFile([
{id: '1.1', source: 'aaaaaaaaaa11', target: 'aaa1', state: 'translated', locations: []},
{id: '1.2', source: 'aaaaaaaaaa22', target: 'aaa2', state: 'translated', locations: [], additionalAttributes: [{name: 'A', value: 'B', path: '.'}]},
{id: '2', source: 'bbbbbb1', target: 'bbb1', state: 'translated', locations: [], additionalAttributes: [{name: 'A2', value: 'B', path: '.'}]},
{id: '3', source: 'ccccccc', target: 'ccc1', state: 'translated', locations: [], additionalAttributes: [{name: 'A3', value: 'B', path: '.'}]},
], 'en', 'de');
const merger = new Merger({}, translationSourceFile, 'new');
const mergedTarget = merger.mergeWithMapping(translationTargetFile, false);
expect(mergedTarget.units).toEqual([
{id: 'a1', source: 'aaaaaaaaaaa1', target: 'aaa1', state: 'new', locations: [], description: undefined, meaning: undefined},
{id: 'a2', source: 'aaaaaaaaaaa2', target: 'aaa2', state: 'new', locations: [], description: undefined, meaning: undefined, additionalAttributes: [{name: 'A', value: 'B', path: '.'}]},
{id: 'b', source: 'bbbbbbb', target: 'bbb1', state: 'new', locations: [], description: undefined, meaning: undefined, additionalAttributes: [{name: 'A2', value: 'B', path: '.'}]},
{id: 'c', source: 'ccccccc', target: 'ccc1', state: 'translated', locations: [], description: undefined, meaning: undefined, additionalAttributes: [{name: 'A3', value: 'B', path: '.'}]},
]);
});
});
});
1 change: 1 addition & 0 deletions src/model/translationFileModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface TranslationUnit {
meaning?: string;
description?: string;
locations: Location[];
additionalAttributes?: { name: string, value: string, path: string }[];
}


Expand Down
87 changes: 87 additions & 0 deletions src/model/translationFileSerialization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ describe('translationFileSerialization', () => {
]
}], 'de', 'fr', '<?xml version="1.0" encoding="UTF-8"?>\n'));
});

it('should parse additionalAttributes', () => {
const xlf2 = `<?xml version="1.0" encoding="UTF-8"?>
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="de" trgLang="fr">
<file id="ngi18n" original="ng.template">
<unit id="ID1">
<segment state="initial" customAttr="customVal">
<source>source val</source>
<target customState="custom">target val</target>
</segment>
</unit>
</file>
</xliff>`;
const translationFile = fromXlf2(xlf2);
expect(translationFile.units[0].additionalAttributes).toEqual([
{name: 'customAttr', value: 'customVal', path: 'segment'},
{name: 'customState', value: 'custom', path: 'segment.target'}
]);
});
});
describe('toXlf2', () => {
it('should keep trailing whitespace', () => {
Expand Down Expand Up @@ -199,6 +218,31 @@ describe('translationFileSerialization', () => {
</xliff>`);
});

it('should format additionalAttributes', () => {
const input = new TranslationFile([{
id: 'ID1',
source: 'source val',
target: 'target val',
state: 'initial',
locations: [],
additionalAttributes: [
{name: 'approved', value: 'yes', path: '.'},
{name: 'other', value: 'value', path: 'segment.target'}
]
}], 'de', 'fr', '<?xml version="1.0" encoding="UTF-8"?>\n');
expect(toXlf2(input, {prettyNestedTags: true})).toEqual(`<?xml version="1.0" encoding="UTF-8"?>
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="de" trgLang="fr">
<file id="ngi18n" original="ng.template">
<unit id="ID1" approved="yes">
<segment state="initial">
<source>source val</source>
<target other="value">target val</target>
</segment>
</unit>
</file>
</xliff>`);
});

});
describe('toXlf1', () => {
it('should not include state attribute if it is undefined', () => {
Expand Down Expand Up @@ -248,6 +292,30 @@ describe('translationFileSerialization', () => {
</trans-unit>
</body>
</file>
</xliff>`);
});
it('should output additinalAttribute', () => {
const input = new TranslationFile([{
id: 'ID1',
source: 'source val',
target: 'target val',
state: 'translated',
locations: [],
additionalAttributes: [
{name: 'approved', value: 'yes', path: '.'},
{name: 'other', value: 'value', path: 'target'}
]
}], 'de', 'fr-ch', '<?xml version="1.0" encoding="UTF-8"?>\n');
expect(toXlf1(input, {prettyNestedTags: true})).toEqual(`<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="de" target-language="fr-ch" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="ID1" datatype="html" approved="yes">
<source>source val</source>
<target state="translated" other="value">target val</target>
</trans-unit>
</body>
</file>
</xliff>`);
});

Expand Down Expand Up @@ -321,5 +389,24 @@ describe('translationFileSerialization', () => {
}]
}], 'de', 'fr-ch', '<?xml version="1.0" encoding="UTF-8"?>\n'));
});

it('should parse additionalAttributes', () => {
const xlf1 = `<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="de" target-language="fr-ch" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="ID1" datatype="html" approved="yes">
<source>source val</source>
<target state="translated" other="value">target val</target>
</trans-unit>
</body>
</file>
</xliff>`;
const translationFile = fromXlf1(xlf1);
expect(translationFile.units[0].additionalAttributes).toEqual([
{name: 'approved', value: 'yes', path: '.'},
{name: 'other', value: 'value', path: 'target'}
]);
});
});
})
59 changes: 56 additions & 3 deletions src/model/translationFileSerialization.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import {XmlDocument, XmlElement, XmlNode, XmlTextNode} from 'xmldoc';
import {TranslationFile} from './translationFileModels';
import {TranslationFile, TranslationUnit} from './translationFileModels';
import {Options} from '../options';


const XML_DECLARATION_MATCHER = /^<\?xml [^>]*>\s*/i;

const REGULAR_ATTRIBUTES_XLF1: {[nodeName: string]: string[]} = {
'trans-unit': ['id', 'datatype'],
'source': [],
'target': ['state'],
'note': ['priority', 'from'],
'context': ['context-type'],
'context-group': ['purpose']
}

const REGULAR_ATTRIBUTES_XLF2: {[nodeName: string]: string[]} = {
'unit': ['id'],
'notes': [],
'note': ['category'],
'segment': ['state'],
'source': [],
'target': []
}

export function fromXlf2(xlf2: string,
options: Pick<Options, 'sortNestedTagAttributes'> = { sortNestedTagAttributes: false }): TranslationFile {

Expand All @@ -16,7 +34,7 @@ export function fromXlf2(xlf2: string,
.map(unit => {
const segment = unit.childNamed('segment')!;
const notes = unit.childNamed('notes');
return {
const result: TranslationUnit = {
id: unit.attr.id,
source: toString(options, ...segment.childNamed('source')!.children),
target: toStringOrUndefined(options, segment.childNamed('target')?.children),
Expand All @@ -36,6 +54,11 @@ export function fromXlf2(xlf2: string,
};
}) ?? []
};
const additionalAttributes = getAdditionalAttributes(unit, REGULAR_ATTRIBUTES_XLF2);
if (additionalAttributes.length) {
result.additionalAttributes = additionalAttributes;
}
return result;
});
return new TranslationFile(units, doc.attr.srcLang, doc.attr.trgLang, xmlDeclaration);
}
Expand All @@ -51,7 +74,7 @@ export function fromXlf1(xlf1: string,
.map(unit => {
const notes = unit.childrenNamed('note');
const target = unit.childNamed('target');
return {
const result: TranslationUnit = {
id: unit.attr.id,
source: toString(options, ...unit.childNamed('source')!.children),
target: toStringOrUndefined(options, target?.children),
Expand All @@ -65,6 +88,11 @@ export function fromXlf1(xlf1: string,
lineStart: parseInt(contextGroup.childWithAttribute('context-type', 'linenumber')!.val, 10)
})) ?? []
};
const additionalAttributes = getAdditionalAttributes(unit, REGULAR_ATTRIBUTES_XLF1);
if (additionalAttributes.length) {
result.additionalAttributes = additionalAttributes;
}
return result;
});
return new TranslationFile(units, file.attr['source-language'], file.attr['target-language'], xmlDeclaration);
}
Expand Down Expand Up @@ -116,6 +144,9 @@ export function toXlf2(translationFile: TranslationFile, options: Pick<Options,
}

updateFirstAndLastChild(u);
unit.additionalAttributes?.forEach(attr => {
(attr.path === '.' ? u : u.descendantWithPath(attr.path)!).attr[attr.name] = attr.value;
});
return u;
});
updateFirstAndLastChild(doc);
Expand Down Expand Up @@ -163,6 +194,9 @@ export function toXlf1(translationFile: TranslationFile, options: Pick<Options,
</context-group>`)));
}
updateFirstAndLastChild(body);
unit.additionalAttributes?.forEach(attr => {
(attr.path === '.' ? transUnit : transUnit.descendantWithPath(attr.path)!).attr[attr.name] = attr.value;
});
return transUnit;
});
return (translationFile.xmlHeader ?? '') + pretty(doc, options);
Expand Down Expand Up @@ -224,3 +258,22 @@ function addPrettyWhitespace(doc: XmlElement, indent: number, options: Pick<Opti
doc.children.forEach(c => c.type === 'element' ? addPrettyWhitespace(c, indent + 1, options) : null);
}
}

function allChildrenWithPath(unit: XmlElement, currentPath = '.'): { element: XmlElement, path: string }[] {
return unit.children.flatMap(child => {
if (child.type === 'element') {
const path = currentPath === '.' ? child.name : (currentPath + '.' + child.name);
return [{element: child, path}, ...allChildrenWithPath(child, path)];
}
return [];
});
}

function getAdditionalAttributes(unit: XmlElement, knownAttributes: {[nodeName: string]: string[]}) {
return [{element: unit, path: '.'}, ...allChildrenWithPath(unit)]
.flatMap(({element, path}) => Object.entries(element.attr)
.map(([attrName, attrValue]) => ({element, attrName, attrValue, path}))
)
.filter(({element, attrName}) => knownAttributes[element.name] ? !knownAttributes[element.name]?.includes(attrName) : false)
.map(({attrName, attrValue, path}) => ({name: attrName, value: attrValue, path}));
}

0 comments on commit f5f837d

Please sign in to comment.