/
xliff1_translation_serializer.ts
150 lines (138 loc) · 5.79 KB
/
xliff1_translation_serializer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
import {FormatOptions, validateOptions} from './format_options';
import {extractIcuPlaceholders} from './icu_parsing';
import {TranslationSerializer} from './translation_serializer';
import {XmlFile} from './xml_file';
/** This is the number of characters that a legacy Xliff 1.2 message id has. */
const LEGACY_XLIFF_MESSAGE_LENGTH = 40;
/**
* A translation serializer that can write XLIFF 1.2 formatted files.
*
* http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
* http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
*
* @see Xliff1TranslationParser
*/
export class Xliff1TranslationSerializer implements TranslationSerializer {
constructor(
private sourceLocale: string, private basePath: AbsoluteFsPath, private useLegacyIds: boolean,
private formatOptions: FormatOptions) {
validateOptions('Xliff1TranslationSerializer', [['xml:space', ['preserve']]], formatOptions);
}
serialize(messages: ɵParsedMessage[]): string {
const ids = new Set<string>();
const xml = new XmlFile();
xml.startTag('xliff', {'version': '1.2', 'xmlns': 'urn:oasis:names:tc:xliff:document:1.2'});
// NOTE: the `original` property is set to the legacy `ng2.template` value for backward
// compatibility.
// We could compute the file from the `message.location` property, but there could
// be multiple values for this in the collection of `messages`. In that case we would probably
// need to change the serializer to output a new `<file>` element for each collection of
// messages that come from a particular original file, and the translation file parsers may not
// be able to cope with this.
xml.startTag('file', {
'source-language': this.sourceLocale,
'datatype': 'plaintext',
'original': 'ng2.template',
...this.formatOptions,
});
xml.startTag('body');
for (const message of messages) {
const id = this.getMessageId(message);
if (ids.has(id)) {
// Do not render the same message more than once
continue;
}
ids.add(id);
xml.startTag('trans-unit', {id, datatype: 'html'});
xml.startTag('source', {}, {preserveWhitespace: true});
this.serializeMessage(xml, message);
xml.endTag('source', {preserveWhitespace: false});
if (message.location) {
this.serializeLocation(xml, message.location);
}
if (message.description) {
this.serializeNote(xml, 'description', message.description);
}
if (message.meaning) {
this.serializeNote(xml, 'meaning', message.meaning);
}
xml.endTag('trans-unit');
}
xml.endTag('body');
xml.endTag('file');
xml.endTag('xliff');
return xml.toString();
}
private serializeMessage(xml: XmlFile, message: ɵParsedMessage): void {
const length = message.messageParts.length - 1;
for (let i = 0; i < length; i++) {
this.serializeTextPart(xml, message.messageParts[i]);
const location = message.substitutionLocations?.[message.placeholderNames[i]];
this.serializePlaceholder(xml, message.placeholderNames[i], location?.text);
}
this.serializeTextPart(xml, message.messageParts[length]);
}
private serializeTextPart(xml: XmlFile, text: string): void {
const pieces = extractIcuPlaceholders(text);
const length = pieces.length - 1;
for (let i = 0; i < length; i += 2) {
xml.text(pieces[i]);
this.serializePlaceholder(xml, pieces[i + 1], undefined);
}
xml.text(pieces[length]);
}
private serializePlaceholder(xml: XmlFile, id: string, text: string|undefined): void {
const attrs: Record<string, string> = {id};
if (text !== undefined) {
attrs['equiv-text'] = text;
}
xml.startTag('x', attrs, {selfClosing: true});
}
private serializeNote(xml: XmlFile, name: string, value: string): void {
xml.startTag('note', {priority: '1', from: name}, {preserveWhitespace: true});
xml.text(value);
xml.endTag('note', {preserveWhitespace: false});
}
private serializeLocation(xml: XmlFile, location: ɵSourceLocation): void {
xml.startTag('context-group', {purpose: 'location'});
this.renderContext(xml, 'sourcefile', relative(this.basePath, location.file));
const endLineString = location.end !== undefined && location.end.line !== location.start.line ?
`,${location.end.line + 1}` :
'';
this.renderContext(xml, 'linenumber', `${location.start.line + 1}${endLineString}`);
xml.endTag('context-group');
}
private renderContext(xml: XmlFile, type: string, value: string): void {
xml.startTag('context', {'context-type': type}, {preserveWhitespace: true});
xml.text(value);
xml.endTag('context', {preserveWhitespace: false});
}
/**
* Get the id for the given `message`.
*
* If there was a custom id provided, use that.
*
* If we have requested legacy message ids, then try to return the appropriate id
* from the list of legacy ids that were extracted.
*
* Otherwise return the canonical message id.
*
* An Xliff 1.2 legacy message id is a hex encoded SHA-1 string, which is 40 characters long. See
* https://csrc.nist.gov/csrc/media/publications/fips/180/4/final/documents/fips180-4-draft-aug2014.pdf
*/
private getMessageId(message: ɵParsedMessage): string {
return message.customId ||
this.useLegacyIds && message.legacyIds !== undefined &&
message.legacyIds.find(id => id.length === LEGACY_XLIFF_MESSAGE_LENGTH) ||
message.id;
}
}