fix(localize): ensure extracted messages are serialized in a consistent order (#40192)
The CLI integration can provide code files in a non-deterministic order, which led to the extracted translation files having messages in a non-consistent order between extractions. This commit fixes this by ensuring that serialized messages are ordered by their location. Fixes #39262 PR Close #40192
This commit is contained in:
parent
805b4f936b
commit
212245f197
|
@ -48,12 +48,13 @@ export class ArbTranslationSerializer implements TranslationSerializer {
|
|||
private sourceLocale: string, private basePath: AbsoluteFsPath, private fs: FileSystem) {}
|
||||
|
||||
serialize(messages: ɵParsedMessage[]): string {
|
||||
const messageMap = consolidateMessages(messages, message => message.customId || message.id);
|
||||
const messageGroups = consolidateMessages(messages, message => getMessageId(message));
|
||||
|
||||
let output = `{\n "@@locale": ${JSON.stringify(this.sourceLocale)}`;
|
||||
|
||||
for (const [id, duplicateMessages] of messageMap.entries()) {
|
||||
for (const duplicateMessages of messageGroups) {
|
||||
const message = duplicateMessages[0];
|
||||
const id = getMessageId(message);
|
||||
output += this.serializeMessage(id, message);
|
||||
output += this.serializeMeta(
|
||||
id, message.description, duplicateMessages.filter(hasLocation).map(m => m.location));
|
||||
|
@ -98,3 +99,7 @@ export class ArbTranslationSerializer implements TranslationSerializer {
|
|||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageId(message: ɵParsedMessage): string {
|
||||
return message.customId || message.id;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
import {ɵMessageId, ɵParsedMessage, ɵSourceMessage} from '@angular/localize';
|
||||
import {TranslationSerializer} from './translation_serializer';
|
||||
import {consolidateMessages} from './utils';
|
||||
|
||||
|
||||
interface SimpleJsonTranslationFile {
|
||||
|
@ -24,7 +25,7 @@ export class SimpleJsonTranslationSerializer implements TranslationSerializer {
|
|||
constructor(private sourceLocale: string) {}
|
||||
serialize(messages: ɵParsedMessage[]): string {
|
||||
const fileObj: SimpleJsonTranslationFile = {locale: this.sourceLocale, translations: {}};
|
||||
for (const message of messages) {
|
||||
for (const [message] of consolidateMessages(messages, message => message.id)) {
|
||||
fileObj.translations[message.id] = message.text;
|
||||
}
|
||||
return JSON.stringify(fileObj, null, 2);
|
||||
|
|
|
@ -8,24 +8,40 @@
|
|||
import {ɵMessageId, ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
|
||||
|
||||
/**
|
||||
* Consolidate an array of messages into a map from message id to an array of messages with that id.
|
||||
* Consolidate messages into groups that have the same id.
|
||||
*
|
||||
* Messages with the same id are grouped together so that we can quickly deduplicate messages when
|
||||
* rendering into translation files.
|
||||
*
|
||||
* To ensure that messages are rendered in a deterministic order:
|
||||
* - the messages within a group are sorted by location (file path, then start position)
|
||||
* - the groups are sorted by the location of the first message in the group
|
||||
*
|
||||
* @param messages the messages to consolidate.
|
||||
* @param getMessageId a function that will compute the message id of a message.
|
||||
* @returns an array of message groups, where each group is an array of messages that have the same
|
||||
* id.
|
||||
*/
|
||||
export function consolidateMessages(
|
||||
messages: ɵParsedMessage[],
|
||||
getMessageId: (message: ɵParsedMessage) => string): Map<ɵMessageId, ɵParsedMessage[]> {
|
||||
const consolidateMessages = new Map<ɵMessageId, ɵParsedMessage[]>();
|
||||
getMessageId: (message: ɵParsedMessage) => string): ɵParsedMessage[][] {
|
||||
const messageGroups = new Map<ɵMessageId, ɵParsedMessage[]>();
|
||||
for (const message of messages) {
|
||||
const id = getMessageId(message);
|
||||
if (!consolidateMessages.has(id)) {
|
||||
consolidateMessages.set(id, [message]);
|
||||
if (!messageGroups.has(id)) {
|
||||
messageGroups.set(id, [message]);
|
||||
} else {
|
||||
consolidateMessages.get(id)!.push(message);
|
||||
messageGroups.get(id)!.push(message);
|
||||
}
|
||||
}
|
||||
return consolidateMessages;
|
||||
|
||||
// Here we sort the messages within a group into location order.
|
||||
// Note that `Array.sort()` will mutate the array in-place.
|
||||
for (const messages of messageGroups.values()) {
|
||||
messages.sort(compareLocations);
|
||||
}
|
||||
// Now we sort the groups by location of the first message in the group.
|
||||
return Array.from(messageGroups.values()).sort((a1, a2) => compareLocations(a1[0], a2[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,3 +51,26 @@ export function hasLocation(message: ɵParsedMessage): message is ɵParsedMessag
|
|||
{location: ɵSourceLocation} {
|
||||
return message.location !== undefined;
|
||||
}
|
||||
|
||||
export function compareLocations(
|
||||
{location: location1}: ɵParsedMessage, {location: location2}: ɵParsedMessage): number {
|
||||
if (location1 === location2) {
|
||||
return 0;
|
||||
}
|
||||
if (location1 === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (location2 === undefined) {
|
||||
return 1;
|
||||
}
|
||||
if (location1.file !== location2.file) {
|
||||
return location1.file < location2.file ? -1 : 1;
|
||||
}
|
||||
if (location1.start.line !== location2.start.line) {
|
||||
return location1.start.line < location2.start.line ? -1 : 1;
|
||||
}
|
||||
if (location1.start.column !== location2.start.column) {
|
||||
return location1.start.column < location2.start.column ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export class Xliff1TranslationSerializer implements TranslationSerializer {
|
|||
}
|
||||
|
||||
serialize(messages: ɵParsedMessage[]): string {
|
||||
const messageMap = consolidateMessages(messages, message => this.getMessageId(message));
|
||||
const messageGroups = consolidateMessages(messages, message => this.getMessageId(message));
|
||||
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
|
||||
|
@ -51,8 +51,9 @@ export class Xliff1TranslationSerializer implements TranslationSerializer {
|
|||
...this.formatOptions,
|
||||
});
|
||||
xml.startTag('body');
|
||||
for (const [id, duplicateMessages] of messageMap.entries()) {
|
||||
for (const duplicateMessages of messageGroups) {
|
||||
const message = duplicateMessages[0];
|
||||
const id = this.getMessageId(message);
|
||||
|
||||
xml.startTag('trans-unit', {id, datatype: 'html'});
|
||||
xml.startTag('source', {}, {preserveWhitespace: true});
|
||||
|
|
|
@ -34,7 +34,7 @@ export class Xliff2TranslationSerializer implements TranslationSerializer {
|
|||
}
|
||||
|
||||
serialize(messages: ɵParsedMessage[]): string {
|
||||
const messageMap = consolidateMessages(messages, message => this.getMessageId(message));
|
||||
const messageGroups = consolidateMessages(messages, message => this.getMessageId(message));
|
||||
const xml = new XmlFile();
|
||||
xml.startTag('xliff', {
|
||||
'version': '2.0',
|
||||
|
@ -49,8 +49,9 @@ export class Xliff2TranslationSerializer implements TranslationSerializer {
|
|||
// messages that come from a particular original file, and the translation file parsers may
|
||||
// not
|
||||
xml.startTag('file', {'id': 'ngi18n', 'original': 'ng.template', ...this.formatOptions});
|
||||
for (const [id, duplicateMessages] of messageMap.entries()) {
|
||||
for (const duplicateMessages of messageGroups) {
|
||||
const message = duplicateMessages[0];
|
||||
const id = this.getMessageId(message);
|
||||
|
||||
xml.startTag('unit', {id});
|
||||
const messagesWithLocations = duplicateMessages.filter(hasLocation);
|
||||
|
|
|
@ -10,6 +10,7 @@ import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
|
|||
|
||||
import {extractIcuPlaceholders} from './icu_parsing';
|
||||
import {TranslationSerializer} from './translation_serializer';
|
||||
import {consolidateMessages} from './utils';
|
||||
import {XmlFile} from './xml_file';
|
||||
|
||||
/**
|
||||
|
@ -26,7 +27,7 @@ export class XmbTranslationSerializer implements TranslationSerializer {
|
|||
private fs: FileSystem = getFileSystem()) {}
|
||||
|
||||
serialize(messages: ɵParsedMessage[]): string {
|
||||
const ids = new Set<string>();
|
||||
const messageGroups = consolidateMessages(messages, message => this.getMessageId(message));
|
||||
const xml = new XmlFile();
|
||||
xml.rawText(
|
||||
`<!DOCTYPE messagebundle [\n` +
|
||||
|
@ -51,13 +52,9 @@ export class XmbTranslationSerializer implements TranslationSerializer {
|
|||
`<!ELEMENT ex (#PCDATA)>\n` +
|
||||
`]>\n`);
|
||||
xml.startTag('messagebundle');
|
||||
for (const message of messages) {
|
||||
for (const duplicateMessages of messageGroups) {
|
||||
const message = duplicateMessages[0];
|
||||
const id = this.getMessageId(message);
|
||||
if (ids.has(id)) {
|
||||
// Do not render the same message more than once
|
||||
continue;
|
||||
}
|
||||
ids.add(id);
|
||||
xml.startTag(
|
||||
'msg', {id, desc: message.description, meaning: message.meaning},
|
||||
{preserveWhitespace: true});
|
||||
|
|
|
@ -464,7 +464,7 @@ runInEachFileSystem(() => {
|
|||
` "locale": "en-GB",`,
|
||||
` "translations": {`,
|
||||
` "message-1": "message {$PH} contents",`,
|
||||
` "message-2": "different message contents"`,
|
||||
` "message-2": "message contents"`,
|
||||
` }`,
|
||||
`}`,
|
||||
].join('\n'));
|
||||
|
@ -489,7 +489,7 @@ runInEachFileSystem(() => {
|
|||
` "locale": "en-GB",`,
|
||||
` "translations": {`,
|
||||
` "message-1": "message {$PH} contents",`,
|
||||
` "message-2": "different message contents"`,
|
||||
` "message-2": "message contents"`,
|
||||
` }`,
|
||||
`}`,
|
||||
].join('\n'));
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
*/
|
||||
import {absoluteFrom, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
|
||||
import {ɵParsedMessage} from '@angular/localize';
|
||||
|
||||
import {ArbTranslationSerializer} from '../../../src/extract/translation_files/arb_translation_serializer';
|
||||
|
||||
import {mockMessage} from './mock_message';
|
||||
import {location, mockMessage} from './mock_message';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
let fs: FileSystem;
|
||||
|
@ -25,30 +25,18 @@ runInEachFileSystem(() => {
|
|||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], {
|
||||
meaning: 'some meaning',
|
||||
location: {
|
||||
file: absoluteFrom('/project/file.ts'),
|
||||
start: {line: 5, column: 10},
|
||||
end: {line: 5, column: 12}
|
||||
},
|
||||
location: location('/project/file.ts', 5, 10, 5, 12),
|
||||
}),
|
||||
mockMessage('54321', ['a', 'b', 'c'], ['PH', 'PH_1'], {
|
||||
customId: 'someId',
|
||||
}),
|
||||
mockMessage('67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {
|
||||
description: 'some description',
|
||||
location: {
|
||||
file: absoluteFrom('/project/file.ts'),
|
||||
start: {line: 5, column: 10},
|
||||
end: {line: 5, column: 12}
|
||||
},
|
||||
location: location('/project/file.ts', 5, 10, 5, 12)
|
||||
}),
|
||||
mockMessage('67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {
|
||||
description: 'some description',
|
||||
location: {
|
||||
file: absoluteFrom('/project/other.ts'),
|
||||
start: {line: 2, column: 10},
|
||||
end: {line: 3, column: 12}
|
||||
},
|
||||
location: location('/project/other.ts', 2, 10, 3, 12)
|
||||
}),
|
||||
mockMessage('13579', ['', 'b', ''], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {}),
|
||||
mockMessage('24680', ['a'], [], {meaning: 'meaning', description: 'and description'}),
|
||||
|
@ -72,6 +60,16 @@ runInEachFileSystem(() => {
|
|||
expect(output.split('\n')).toEqual([
|
||||
'{',
|
||||
' "@@locale": "xx",',
|
||||
' "someId": "a{$PH}b{$PH_1}c",',
|
||||
' "13579": "{$START_BOLD_TEXT}b{$CLOSE_BOLD_TEXT}",',
|
||||
' "24680": "a",',
|
||||
' "@24680": {',
|
||||
' "description": "and description"',
|
||||
' },',
|
||||
' "80808": "multi\\nlines",',
|
||||
' "90000": "<escape{$double-quotes-\\"}me>",',
|
||||
' "100000": "pre-ICU {VAR_SELECT, select, a {a} b {{INTERPOLATION}} c {pre {INTERPOLATION_1} post}} post-ICU",',
|
||||
' "100001": "{VAR_PLURAL, plural, one {{START_BOLD_TEXT}something bold{CLOSE_BOLD_TEXT}} other {pre {START_TAG_SPAN}middle{CLOSE_TAG_SPAN} post}}",',
|
||||
' "12345": "a{$PH}b{$PH_1}c",',
|
||||
' "@12345": {',
|
||||
' "x-locations": [',
|
||||
|
@ -82,7 +80,6 @@ runInEachFileSystem(() => {
|
|||
' }',
|
||||
' ]',
|
||||
' },',
|
||||
' "someId": "a{$PH}b{$PH_1}c",',
|
||||
' "67890": "a{$START_TAG_SPAN}{$CLOSE_TAG_SPAN}c",',
|
||||
' "@67890": {',
|
||||
' "description": "some description",',
|
||||
|
@ -98,16 +95,89 @@ runInEachFileSystem(() => {
|
|||
' "end": { "line": "3", "column": "12" }',
|
||||
' }',
|
||||
' ]',
|
||||
' }',
|
||||
'}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should consistently order serialized messages by location', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/c-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/c-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 8, 0, 10, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 8, 0, 10, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/a-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/a-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 5, 20, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 5, 20, 5, 12)}),
|
||||
];
|
||||
const serializer = new ArbTranslationSerializer('xx', fs.resolve('/root'), fs);
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output.split('\n')).toEqual([
|
||||
'{',
|
||||
' "@@locale": "xx",',
|
||||
' "1": "message-1",',
|
||||
' "@1": {',
|
||||
' "x-locations": [',
|
||||
' {',
|
||||
' "file": "a-1.ts",',
|
||||
' "start": { "line": "5", "column": "10" },',
|
||||
' "end": { "line": "5", "column": "12" }',
|
||||
' },',
|
||||
' {',
|
||||
' "file": "b-1.ts",',
|
||||
' "start": { "line": "5", "column": "10" },',
|
||||
' "end": { "line": "5", "column": "12" }',
|
||||
' },',
|
||||
' {',
|
||||
' "file": "b-1.ts",',
|
||||
' "start": { "line": "5", "column": "20" },',
|
||||
' "end": { "line": "5", "column": "12" }',
|
||||
' },',
|
||||
' {',
|
||||
' "file": "b-1.ts",',
|
||||
' "start": { "line": "8", "column": "0" },',
|
||||
' "end": { "line": "10", "column": "12" }',
|
||||
' },',
|
||||
' {',
|
||||
' "file": "c-1.ts",',
|
||||
' "start": { "line": "5", "column": "10" },',
|
||||
' "end": { "line": "5", "column": "12" }',
|
||||
' }',
|
||||
' ]',
|
||||
' },',
|
||||
' "13579": "{$START_BOLD_TEXT}b{$CLOSE_BOLD_TEXT}",',
|
||||
' "24680": "a",',
|
||||
' "@24680": {',
|
||||
' "description": "and description"',
|
||||
' },',
|
||||
' "80808": "multi\\nlines",',
|
||||
' "90000": "<escape{$double-quotes-\\"}me>",',
|
||||
' "100000": "pre-ICU {VAR_SELECT, select, a {a} b {{INTERPOLATION}} c {pre {INTERPOLATION_1} post}} post-ICU",',
|
||||
' "100001": "{VAR_PLURAL, plural, one {{START_BOLD_TEXT}something bold{CLOSE_BOLD_TEXT}} other {pre {START_TAG_SPAN}middle{CLOSE_TAG_SPAN} post}}"',
|
||||
' "2": "message-1",',
|
||||
' "@2": {',
|
||||
' "x-locations": [',
|
||||
' {',
|
||||
' "file": "a-2.ts",',
|
||||
' "start": { "line": "5", "column": "10" },',
|
||||
' "end": { "line": "5", "column": "12" }',
|
||||
' },',
|
||||
' {',
|
||||
' "file": "b-2.ts",',
|
||||
' "start": { "line": "5", "column": "10" },',
|
||||
' "end": { "line": "5", "column": "12" }',
|
||||
' },',
|
||||
' {',
|
||||
' "file": "b-2.ts",',
|
||||
' "start": { "line": "5", "column": "20" },',
|
||||
' "end": { "line": "5", "column": "12" }',
|
||||
' },',
|
||||
' {',
|
||||
' "file": "b-2.ts",',
|
||||
' "start": { "line": "8", "column": "0" },',
|
||||
' "end": { "line": "10", "column": "12" }',
|
||||
' },',
|
||||
' {',
|
||||
' "file": "c-2.ts",',
|
||||
' "start": { "line": "5", "column": "10" },',
|
||||
' "end": { "line": "5", "column": "12" }',
|
||||
' }',
|
||||
' ]',
|
||||
' }',
|
||||
'}',
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 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 {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {ɵParsedMessage} from '@angular/localize';
|
||||
import {MessageId, SourceLocation} from '@angular/localize/src/utils';
|
||||
|
||||
|
@ -38,3 +39,13 @@ export function mockMessage(
|
|||
placeholderNames,
|
||||
};
|
||||
}
|
||||
|
||||
export function location(
|
||||
file: string, startLine: number, startCol: number, endLine: number,
|
||||
endCol: number): SourceLocation {
|
||||
return {
|
||||
file: absoluteFrom(file),
|
||||
start: {line: startLine, column: startCol},
|
||||
end: {line: endLine, column: endCol}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
|
|||
import {FormatOptions} from '../../../src/extract/translation_files/format_options';
|
||||
import {Xliff1TranslationSerializer} from '../../../src/extract/translation_files/xliff1_translation_serializer';
|
||||
|
||||
import {mockMessage} from './mock_message';
|
||||
import {location, mockMessage} from './mock_message';
|
||||
import {toAttributes} from './utils';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
|
@ -85,16 +85,6 @@ runInEachFileSystem(() => {
|
|||
` <file source-language="xx" datatype="plaintext" original="ng2.template"${
|
||||
toAttributes(options)}>`,
|
||||
` <body>`,
|
||||
` <trans-unit id="${
|
||||
useLegacyIds ? '1234567890ABCDEF1234567890ABCDEF12345678' :
|
||||
'12345'}" datatype="html">`,
|
||||
` <source>a<x id="PH"/>b<x id="PH_1"/>c</source>`,
|
||||
` <context-group purpose="location">`,
|
||||
` <context context-type="sourcefile">file.ts</context>`,
|
||||
` <context context-type="linenumber">6</context>`,
|
||||
` </context-group>`,
|
||||
` <note priority="1" from="meaning">some meaning</note>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="someId" datatype="html">`,
|
||||
` <source>a<x id="PH" equiv-text="placeholder + 1"/>b<x id="PH_1"/>c</source>`,
|
||||
` </trans-unit>`,
|
||||
|
@ -102,13 +92,6 @@ runInEachFileSystem(() => {
|
|||
` <source>a<x id="START_TAG_SPAN" ctype="x-span"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/>c</source>`,
|
||||
` <note priority="1" from="description">some description</note>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="38705" datatype="html">`,
|
||||
` <source>a<x id="START_TAG_SPAN" ctype="x-span"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/>c</source>`,
|
||||
` <context-group purpose="location">`,
|
||||
` <context context-type="sourcefile">file.ts</context>`,
|
||||
` <context context-type="linenumber">3,4</context>`,
|
||||
` </context-group>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="13579" datatype="html">`,
|
||||
` <source><x id="START_BOLD_TEXT" ctype="x-b"/>b<x id="CLOSE_BOLD_TEXT" ctype="x-b"/></source>`,
|
||||
` </trans-unit>`,
|
||||
|
@ -130,6 +113,23 @@ runInEachFileSystem(() => {
|
|||
` <trans-unit id="100001" datatype="html">`,
|
||||
` <source>{VAR_PLURAL, plural, one {<x id="START_BOLD_TEXT" ctype="x-b"/>something bold<x id="CLOSE_BOLD_TEXT" ctype="x-b"/>} other {pre <x id="START_TAG_SPAN" ctype="x-span"/>middle<x id="CLOSE_TAG_SPAN" ctype="x-span"/> post}}</source>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="38705" datatype="html">`,
|
||||
` <source>a<x id="START_TAG_SPAN" ctype="x-span"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/>c</source>`,
|
||||
` <context-group purpose="location">`,
|
||||
` <context context-type="sourcefile">file.ts</context>`,
|
||||
` <context context-type="linenumber">3,4</context>`,
|
||||
` </context-group>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="${
|
||||
useLegacyIds ? '1234567890ABCDEF1234567890ABCDEF12345678' :
|
||||
'12345'}" datatype="html">`,
|
||||
` <source>a<x id="PH"/>b<x id="PH_1"/>c</source>`,
|
||||
` <context-group purpose="location">`,
|
||||
` <context context-type="sourcefile">file.ts</context>`,
|
||||
` <context context-type="linenumber">6</context>`,
|
||||
` </context-group>`,
|
||||
` <note priority="1" from="meaning">some meaning</note>`,
|
||||
` </trans-unit>`,
|
||||
` </body>`,
|
||||
` </file>`,
|
||||
`</xliff>\n`,
|
||||
|
@ -291,5 +291,80 @@ runInEachFileSystem(() => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderFile()', () => {
|
||||
it('should consistently order serialized messages by location', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/c-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/c-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 8, 0, 10, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 8, 0, 10, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/a-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/a-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 5, 20, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 5, 20, 5, 12)}),
|
||||
];
|
||||
const serializer = new Xliff1TranslationSerializer('xx', absoluteFrom('/root'), false, {});
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output.split('\n')).toEqual([
|
||||
'<?xml version="1.0" encoding="UTF-8" ?>',
|
||||
'<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">',
|
||||
' <file source-language="xx" datatype="plaintext" original="ng2.template">',
|
||||
' <body>',
|
||||
' <trans-unit id="1" datatype="html">',
|
||||
' <source>message-1</source>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">a-1.ts</context>',
|
||||
' <context context-type="linenumber">6</context>',
|
||||
' </context-group>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">b-1.ts</context>',
|
||||
' <context context-type="linenumber">6</context>',
|
||||
' </context-group>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">b-1.ts</context>',
|
||||
' <context context-type="linenumber">6</context>',
|
||||
' </context-group>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">b-1.ts</context>',
|
||||
' <context context-type="linenumber">9,11</context>',
|
||||
' </context-group>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">c-1.ts</context>',
|
||||
' <context context-type="linenumber">6</context>',
|
||||
' </context-group>',
|
||||
' </trans-unit>',
|
||||
' <trans-unit id="2" datatype="html">',
|
||||
' <source>message-1</source>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">a-2.ts</context>',
|
||||
' <context context-type="linenumber">6</context>',
|
||||
' </context-group>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">b-2.ts</context>',
|
||||
' <context context-type="linenumber">6</context>',
|
||||
' </context-group>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">b-2.ts</context>',
|
||||
' <context context-type="linenumber">6</context>',
|
||||
' </context-group>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">b-2.ts</context>',
|
||||
' <context context-type="linenumber">9,11</context>',
|
||||
' </context-group>',
|
||||
' <context-group purpose="location">',
|
||||
' <context context-type="sourcefile">c-2.ts</context>',
|
||||
' <context context-type="linenumber">6</context>',
|
||||
' </context-group>',
|
||||
' </trans-unit>',
|
||||
' </body>',
|
||||
' </file>',
|
||||
'</xliff>',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
|
|||
import {FormatOptions} from '../../../src/extract/translation_files/format_options';
|
||||
import {Xliff2TranslationSerializer} from '../../../src/extract/translation_files/xliff2_translation_serializer';
|
||||
|
||||
import {mockMessage} from './mock_message';
|
||||
import {location, mockMessage} from './mock_message';
|
||||
import {toAttributes} from './utils';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
|
@ -88,37 +88,11 @@ runInEachFileSystem(() => {
|
|||
`<?xml version="1.0" encoding="UTF-8" ?>`,
|
||||
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="xx">`,
|
||||
` <file id="ngi18n" original="ng.template"${toAttributes(options)}>`,
|
||||
` <unit id="${useLegacyIds ? '615790887472569365' : '12345'}">`,
|
||||
` <notes>`,
|
||||
` <note category="location">file.ts:6</note>`,
|
||||
` <note category="meaning">some meaning</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>a<ph id="0" equiv="PH"/>b<ph id="1" equiv="PH_1"/>c</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="someId">`,
|
||||
` <segment>`,
|
||||
` <source>a<ph id="0" equiv="PH" disp="placeholder + 1"/>b<ph id="1" equiv="PH_1"/>c</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="67890">`,
|
||||
` <notes>`,
|
||||
` <note category="location">file.ts:3,4</note>`,
|
||||
` <note category="description">some description</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other"></pc>c</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="location-only">`,
|
||||
` <notes>`,
|
||||
` <note category="location">file.ts:3,4</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other"></pc>c</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="13579">`,
|
||||
` <segment>`,
|
||||
` <source><pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt">b</pc></source>`,
|
||||
|
@ -154,6 +128,32 @@ runInEachFileSystem(() => {
|
|||
` <source>{VAR_PLURAL, plural, one {<pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt">something bold</pc>} other {pre <pc id="1" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other">middle</pc> post}}</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="67890">`,
|
||||
` <notes>`,
|
||||
` <note category="location">file.ts:3,4</note>`,
|
||||
` <note category="description">some description</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other"></pc>c</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="location-only">`,
|
||||
` <notes>`,
|
||||
` <note category="location">file.ts:3,4</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other"></pc>c</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="${useLegacyIds ? '615790887472569365' : '12345'}">`,
|
||||
` <notes>`,
|
||||
` <note category="location">file.ts:6</note>`,
|
||||
` <note category="meaning">some meaning</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>a<ph id="0" equiv="PH"/>b<ph id="1" equiv="PH_1"/>c</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` </file>`,
|
||||
`</xliff>\n`,
|
||||
].join('\n'));
|
||||
|
@ -304,5 +304,56 @@ runInEachFileSystem(() => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderFile()', () => {
|
||||
it('should consistently order serialized messages by location', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/c-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/c-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 8, 0, 10, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 8, 0, 10, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/a-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/a-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 5, 20, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 5, 20, 5, 12)}),
|
||||
];
|
||||
const serializer = new Xliff2TranslationSerializer('xx', absoluteFrom('/root'), false, {});
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output.split('\n')).toEqual([
|
||||
'<?xml version="1.0" encoding="UTF-8" ?>',
|
||||
'<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="xx">',
|
||||
' <file id="ngi18n" original="ng.template">',
|
||||
' <unit id="1">',
|
||||
' <notes>',
|
||||
' <note category="location">a-1.ts:6</note>',
|
||||
' <note category="location">b-1.ts:6</note>',
|
||||
' <note category="location">b-1.ts:6</note>',
|
||||
' <note category="location">b-1.ts:9,11</note>',
|
||||
' <note category="location">c-1.ts:6</note>',
|
||||
' </notes>',
|
||||
' <segment>',
|
||||
' <source>message-1</source>',
|
||||
' </segment>',
|
||||
' </unit>',
|
||||
' <unit id="2">',
|
||||
' <notes>',
|
||||
' <note category="location">a-2.ts:6</note>',
|
||||
' <note category="location">b-2.ts:6</note>',
|
||||
' <note category="location">b-2.ts:6</note>',
|
||||
' <note category="location">b-2.ts:9,11</note>',
|
||||
' <note category="location">c-2.ts:6</note>',
|
||||
' </notes>',
|
||||
' <segment>',
|
||||
' <source>message-1</source>',
|
||||
' </segment>',
|
||||
' </unit>',
|
||||
' </file>',
|
||||
'</xliff>',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
|
|||
|
||||
import {XmbTranslationSerializer} from '../../../src/extract/translation_files/xmb_translation_serializer';
|
||||
|
||||
import {mockMessage} from './mock_message';
|
||||
import {location, mockMessage} from './mock_message';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
let fs: FileSystem;
|
||||
|
@ -86,4 +86,52 @@ runInEachFileSystem(() => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderFile()', () => {
|
||||
it('should consistently order serialized messages by location', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/c-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/c-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 8, 0, 10, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 8, 0, 10, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/a-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/a-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 5, 10, 5, 12)}),
|
||||
mockMessage('1', ['message-1'], [], {location: location('/root/b-1.ts', 5, 20, 5, 12)}),
|
||||
mockMessage('2', ['message-1'], [], {location: location('/root/b-2.ts', 5, 20, 5, 12)}),
|
||||
];
|
||||
const serializer = new XmbTranslationSerializer(absoluteFrom('/root'), false);
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output.split('\n')).toEqual([
|
||||
'<?xml version="1.0" encoding="UTF-8" ?>',
|
||||
'<!DOCTYPE messagebundle [',
|
||||
'<!ELEMENT messagebundle (msg)*>',
|
||||
'<!ATTLIST messagebundle class CDATA #IMPLIED>',
|
||||
'',
|
||||
'<!ELEMENT msg (#PCDATA|ph|source)*>',
|
||||
'<!ATTLIST msg id CDATA #IMPLIED>',
|
||||
'<!ATTLIST msg seq CDATA #IMPLIED>',
|
||||
'<!ATTLIST msg name CDATA #IMPLIED>',
|
||||
'<!ATTLIST msg desc CDATA #IMPLIED>',
|
||||
'<!ATTLIST msg meaning CDATA #IMPLIED>',
|
||||
'<!ATTLIST msg obsolete (obsolete) #IMPLIED>',
|
||||
'<!ATTLIST msg xml:space (default|preserve) "default">',
|
||||
'<!ATTLIST msg is_hidden CDATA #IMPLIED>',
|
||||
'',
|
||||
'<!ELEMENT source (#PCDATA)>',
|
||||
'',
|
||||
'<!ELEMENT ph (#PCDATA|ex)*>',
|
||||
'<!ATTLIST ph name CDATA #REQUIRED>',
|
||||
'',
|
||||
'<!ELEMENT ex (#PCDATA)>',
|
||||
']>',
|
||||
'<messagebundle>',
|
||||
' <msg id="1"><source>a-1.ts:5</source>message-1</msg>',
|
||||
' <msg id="2"><source>a-2.ts:5</source>message-1</msg>',
|
||||
'</messagebundle>',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue