fix(localize): enable whitespace preservation marker in XLIFF files (#38737)

Whitespace can be relevant in extracted XLIFF translation files.
Some i18n tools - e.g. CAT tool (OmegaT) - will reformat
the file to collapse whitespace if there is no indication to tell it
not to.

This commit adds the ability to specify "format options" that are passed
to the translation file serializer. The XLIFF 1.2 and 2.0 seralizers have
been updated to accept `{"xml:space":"preserve"}` format option which will
by added to the `<file>` element in the serialized translation file during
extraction.

Fixes #38679

PR Close #38737
This commit is contained in:
Pete Bacon Darwin 2020-09-07 21:16:55 +01:00 committed by atscott
parent c880e393e9
commit 4360eed9b7
11 changed files with 556 additions and 402 deletions

View File

@ -21,6 +21,7 @@ import {SimpleJsonTranslationSerializer} from './translation_files/json_translat
import {Xliff1TranslationSerializer} from './translation_files/xliff1_translation_serializer'; import {Xliff1TranslationSerializer} from './translation_files/xliff1_translation_serializer';
import {Xliff2TranslationSerializer} from './translation_files/xliff2_translation_serializer'; import {Xliff2TranslationSerializer} from './translation_files/xliff2_translation_serializer';
import {XmbTranslationSerializer} from './translation_files/xmb_translation_serializer'; import {XmbTranslationSerializer} from './translation_files/xmb_translation_serializer';
import {FormatOptions, parseFormatOptions} from './translation_files/format_options';
if (require.main === module) { if (require.main === module) {
const args = process.argv.slice(2); const args = process.argv.slice(2);
@ -54,6 +55,13 @@ if (require.main === module) {
describe: 'The format of the translation file.', describe: 'The format of the translation file.',
type: 'string', type: 'string',
}) })
.option('formatOptions', {
describe:
'Additional options to pass to the translation file serializer, in the form of JSON formatted key-value string pairs:\n' +
'For example: `--formatOptions {"xml:space":"preserve"}.\n' +
'The meaning of the options is specific to the format being serialized.',
type: 'string'
})
.option('o', { .option('o', {
alias: 'outputPath', alias: 'outputPath',
required: true, required: true,
@ -97,6 +105,7 @@ if (require.main === module) {
const logLevel = options.loglevel as (keyof typeof LogLevel) | undefined; const logLevel = options.loglevel as (keyof typeof LogLevel) | undefined;
const logger = new ConsoleLogger(logLevel ? LogLevel[logLevel] : LogLevel.warn); const logger = new ConsoleLogger(logLevel ? LogLevel[logLevel] : LogLevel.warn);
const duplicateMessageHandling = options.d as DiagnosticHandlingStrategy; const duplicateMessageHandling = options.d as DiagnosticHandlingStrategy;
const formatOptions = parseFormatOptions(options.formatOptions);
extractTranslations({ extractTranslations({
@ -109,6 +118,7 @@ if (require.main === module) {
useSourceMaps: options.useSourceMaps, useSourceMaps: options.useSourceMaps,
useLegacyIds: options.useLegacyIds, useLegacyIds: options.useLegacyIds,
duplicateMessageHandling, duplicateMessageHandling,
formatOptions,
}); });
} }
@ -152,6 +162,10 @@ export interface ExtractTranslationsOptions {
* How to handle messages with the same id but not the same text. * How to handle messages with the same id but not the same text.
*/ */
duplicateMessageHandling: DiagnosticHandlingStrategy; duplicateMessageHandling: DiagnosticHandlingStrategy;
/**
* A collection of formatting options to pass to the translation file serializer.
*/
formatOptions?: FormatOptions;
} }
export function extractTranslations({ export function extractTranslations({
@ -164,6 +178,7 @@ export function extractTranslations({
useSourceMaps, useSourceMaps,
useLegacyIds, useLegacyIds,
duplicateMessageHandling, duplicateMessageHandling,
formatOptions = {},
}: ExtractTranslationsOptions) { }: ExtractTranslationsOptions) {
const fs = getFileSystem(); const fs = getFileSystem();
const basePath = fs.resolve(rootPath); const basePath = fs.resolve(rootPath);
@ -180,7 +195,8 @@ export function extractTranslations({
} }
const outputPath = fs.resolve(rootPath, output); const outputPath = fs.resolve(rootPath, output);
const serializer = getSerializer(format, sourceLocale, fs.dirname(outputPath), useLegacyIds); const serializer =
getSerializer(format, sourceLocale, fs.dirname(outputPath), useLegacyIds, formatOptions);
const translationFile = serializer.serialize(messages); const translationFile = serializer.serialize(messages);
fs.ensureDir(fs.dirname(outputPath)); fs.ensureDir(fs.dirname(outputPath));
fs.writeFile(outputPath, translationFile); fs.writeFile(outputPath, translationFile);
@ -191,17 +207,17 @@ export function extractTranslations({
} }
export function getSerializer( export function getSerializer(
format: string, sourceLocale: string, rootPath: AbsoluteFsPath, format: string, sourceLocale: string, rootPath: AbsoluteFsPath, useLegacyIds: boolean,
useLegacyIds: boolean): TranslationSerializer { formatOptions: FormatOptions): TranslationSerializer {
switch (format) { switch (format) {
case 'xlf': case 'xlf':
case 'xlif': case 'xlif':
case 'xliff': case 'xliff':
return new Xliff1TranslationSerializer(sourceLocale, rootPath, useLegacyIds); return new Xliff1TranslationSerializer(sourceLocale, rootPath, useLegacyIds, formatOptions);
case 'xlf2': case 'xlf2':
case 'xlif2': case 'xlif2':
case 'xliff2': case 'xliff2':
return new Xliff2TranslationSerializer(sourceLocale, rootPath, useLegacyIds); return new Xliff2TranslationSerializer(sourceLocale, rootPath, useLegacyIds, formatOptions);
case 'xmb': case 'xmb':
return new XmbTranslationSerializer(rootPath, useLegacyIds); return new XmbTranslationSerializer(rootPath, useLegacyIds);
case 'json': case 'json':

View File

@ -0,0 +1,44 @@
/**
* @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
*/
export type FormatOptions = Record<string, string>;
export type ValidOption = [key: string, values: string[]];
export type ValidOptions = ValidOption[];
/**
* Check that the given `options` are allowed based on the given `validOptions`.
* @param name The name of the serializer that is receiving the options.
* @param validOptions An array of valid options and their allowed values.
* @param options The options to be validated.
*/
export function validateOptions(name: string, validOptions: ValidOptions, options: FormatOptions) {
const validOptionsMap = new Map<ValidOption[0], ValidOption[1]>(validOptions);
for (const option in options) {
if (!validOptionsMap.has(option)) {
throw new Error(
`Invalid format option for ${name}: "${option}".\n` +
`Allowed options are ${JSON.stringify(Array.from(validOptionsMap.keys()))}.`);
}
const validOptionValues = validOptionsMap.get(option)!;
const optionValue = options[option];
if (!validOptionValues.includes(optionValue)) {
throw new Error(
`Invalid format option value for ${name}: "${option}".\n` +
`Allowed option values are ${JSON.stringify(validOptionValues)} but received "${
optionValue}".`);
}
}
}
/**
* Parse the given `optionString` into a collection of `FormatOptions`.
* @param optionString The string to parse.
*/
export function parseFormatOptions(optionString: string = '{}'): FormatOptions {
return JSON.parse(optionString);
}

View File

@ -8,6 +8,7 @@
import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system'; import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
import {FormatOptions, validateOptions} from './format_options';
import {extractIcuPlaceholders} from './icu_parsing'; import {extractIcuPlaceholders} from './icu_parsing';
import {TranslationSerializer} from './translation_serializer'; import {TranslationSerializer} from './translation_serializer';
import {XmlFile} from './xml_file'; import {XmlFile} from './xml_file';
@ -25,8 +26,10 @@ const LEGACY_XLIFF_MESSAGE_LENGTH = 40;
*/ */
export class Xliff1TranslationSerializer implements TranslationSerializer { export class Xliff1TranslationSerializer implements TranslationSerializer {
constructor( constructor(
private sourceLocale: string, private basePath: AbsoluteFsPath, private sourceLocale: string, private basePath: AbsoluteFsPath, private useLegacyIds: boolean,
private useLegacyIds: boolean) {} private formatOptions: FormatOptions) {
validateOptions('Xliff1TranslationSerializer', [['xml:space', ['preserve']]], formatOptions);
}
serialize(messages: ɵParsedMessage[]): string { serialize(messages: ɵParsedMessage[]): string {
const ids = new Set<string>(); const ids = new Set<string>();
@ -43,6 +46,7 @@ export class Xliff1TranslationSerializer implements TranslationSerializer {
'source-language': this.sourceLocale, 'source-language': this.sourceLocale,
'datatype': 'plaintext', 'datatype': 'plaintext',
'original': 'ng2.template', 'original': 'ng2.template',
...this.formatOptions,
}); });
xml.startTag('body'); xml.startTag('body');
for (const message of messages) { for (const message of messages) {

View File

@ -8,6 +8,7 @@
import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system'; import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
import {FormatOptions, validateOptions} from './format_options';
import {extractIcuPlaceholders} from './icu_parsing'; import {extractIcuPlaceholders} from './icu_parsing';
import {TranslationSerializer} from './translation_serializer'; import {TranslationSerializer} from './translation_serializer';
import {XmlFile} from './xml_file'; import {XmlFile} from './xml_file';
@ -25,8 +26,10 @@ const MAX_LEGACY_XLIFF_2_MESSAGE_LENGTH = 20;
export class Xliff2TranslationSerializer implements TranslationSerializer { export class Xliff2TranslationSerializer implements TranslationSerializer {
private currentPlaceholderId = 0; private currentPlaceholderId = 0;
constructor( constructor(
private sourceLocale: string, private basePath: AbsoluteFsPath, private sourceLocale: string, private basePath: AbsoluteFsPath, private useLegacyIds: boolean,
private useLegacyIds: boolean) {} private formatOptions: FormatOptions) {
validateOptions('Xliff1TranslationSerializer', [['xml:space', ['preserve']]], formatOptions);
}
serialize(messages: ɵParsedMessage[]): string { serialize(messages: ɵParsedMessage[]): string {
const ids = new Set<string>(); const ids = new Set<string>();
@ -41,8 +44,9 @@ export class Xliff2TranslationSerializer implements TranslationSerializer {
// We could compute the file from the `message.location` property, but there could // 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 // 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 // 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 // messages that come from a particular original file, and the translation file parsers may
xml.startTag('file', {'id': 'ngi18n', 'original': 'ng.template'}); // not
xml.startTag('file', {'id': 'ngi18n', 'original': 'ng.template', ...this.formatOptions});
for (const message of messages) { for (const message of messages) {
const id = this.getMessageId(message); const id = this.getMessageId(message);
if (ids.has(id)) { if (ids.has(id)) {

View File

@ -6,6 +6,7 @@ ts_library(
srcs = glob( srcs = glob(
["**/*.ts"], ["**/*.ts"],
), ),
visibility = ["//packages/localize/src/tools/test:__subpackages__"],
deps = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler", "//packages/compiler",

View File

@ -14,6 +14,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/logging/testing", "//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/compiler-cli/test/helpers", "//packages/compiler-cli/test/helpers",
"//packages/localize/src/tools", "//packages/localize/src/tools",
"//packages/localize/src/tools/test:test_lib",
], ],
) )

View File

@ -11,6 +11,8 @@ import {MockLogger} from '@angular/compiler-cli/src/ngtsc/logging/testing';
import {loadTestDirectory} from '@angular/compiler-cli/test/helpers'; import {loadTestDirectory} from '@angular/compiler-cli/test/helpers';
import {extractTranslations} from '../../../src/extract/main'; import {extractTranslations} from '../../../src/extract/main';
import {FormatOptions} from '../../../src/extract/translation_files/format_options';
import {toAttributes} from '../translation_files/utils';
runInEachFileSystem(() => { runInEachFileSystem(() => {
let fs: FileSystem; let fs: FileSystem;
@ -134,145 +136,152 @@ runInEachFileSystem(() => {
].join('\n')); ].join('\n'));
}); });
it('should extract translations from source code, and write as XLIFF 1.2 format', () => { for (const formatOptions of [{}, {'xml:space': 'preserve'}] as FormatOptions[]) {
extractTranslations({ it(`should extract translations from source code, and write as XLIFF 1.2 format${
rootPath, formatOptions['xml:space'] ? '[with xml:space attribute]' : ''}`,
sourceLocale: 'en-CA', () => {
sourceFilePaths: [sourceFilePath], extractTranslations({
format: 'xliff', rootPath,
outputPath, sourceLocale: 'en-CA',
logger, sourceFilePaths: [sourceFilePath],
useSourceMaps: false, format: 'xliff',
useLegacyIds, outputPath,
duplicateMessageHandling: 'ignore', logger,
}); useSourceMaps: false,
expect(fs.readFile(outputPath)).toEqual([ useLegacyIds,
`<?xml version="1.0" encoding="UTF-8" ?>`, duplicateMessageHandling: 'ignore',
`<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">`, formatOptions,
` <file source-language="en-CA" datatype="plaintext" original="ng2.template">`, });
` <body>`, expect(fs.readFile(outputPath)).toEqual([
` <trans-unit id="3291030485717846467" datatype="html">`, `<?xml version="1.0" encoding="UTF-8" ?>`,
` <source>Hello, <x id="PH" equiv-text="name"/>!</source>`, `<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">`,
` <context-group purpose="location">`, ` <file source-language="en-CA" datatype="plaintext" original="ng2.template"${
` <context context-type="sourcefile">test_files/test.js</context>`, toAttributes(formatOptions)}>`,
` <context context-type="linenumber">2</context>`, ` <body>`,
` </context-group>`, ` <trans-unit id="3291030485717846467" datatype="html">`,
` </trans-unit>`, ` <source>Hello, <x id="PH" equiv-text="name"/>!</source>`,
` <trans-unit id="8669027859022295761" datatype="html">`, ` <context-group purpose="location">`,
` <source>try<x id="PH" equiv-text="40 + 2"/>me</source>`, ` <context context-type="sourcefile">test_files/test.js</context>`,
` <context-group purpose="location">`, ` <context context-type="linenumber">2</context>`,
` <context context-type="sourcefile">test_files/test.js</context>`, ` </context-group>`,
` <context context-type="linenumber">3</context>`, ` </trans-unit>`,
` </context-group>`, ` <trans-unit id="8669027859022295761" datatype="html">`,
` </trans-unit>`, ` <source>try<x id="PH" equiv-text="40 + 2"/>me</source>`,
` <trans-unit id="custom-id" datatype="html">`, ` <context-group purpose="location">`,
` <source>Custom id message</source>`, ` <context context-type="sourcefile">test_files/test.js</context>`,
` <context-group purpose="location">`, ` <context context-type="linenumber">3</context>`,
` <context context-type="sourcefile">test_files/test.js</context>`, ` </context-group>`,
` <context context-type="linenumber">4</context>`, ` </trans-unit>`,
` </context-group>`, ` <trans-unit id="custom-id" datatype="html">`,
` </trans-unit>`, ` <source>Custom id message</source>`,
` <trans-unit id="${ ` <context-group purpose="location">`,
useLegacyIds ? '1234567890123456789012345678901234567890' : ` <context context-type="sourcefile">test_files/test.js</context>`,
'273296103957933077'}" datatype="html">`, ` <context context-type="linenumber">4</context>`,
` <source>Legacy id message</source>`, ` </context-group>`,
` <context-group purpose="location">`, ` </trans-unit>`,
` <context context-type="sourcefile">test_files/test.js</context>`, ` <trans-unit id="${
` <context context-type="linenumber">6</context>`, useLegacyIds ? '1234567890123456789012345678901234567890' :
` </context-group>`, '273296103957933077'}" datatype="html">`,
` </trans-unit>`, ` <source>Legacy id message</source>`,
` <trans-unit id="custom-id-2" datatype="html">`, ` <context-group purpose="location">`,
` <source>Custom and legacy message</source>`, ` <context context-type="sourcefile">test_files/test.js</context>`,
` <context-group purpose="location">`, ` <context context-type="linenumber">6</context>`,
` <context context-type="sourcefile">test_files/test.js</context>`, ` </context-group>`,
` <context context-type="linenumber">8</context>`, ` </trans-unit>`,
` </context-group>`, ` <trans-unit id="custom-id-2" datatype="html">`,
` </trans-unit>`, ` <source>Custom and legacy message</source>`,
` <trans-unit id="2932901491976224757" datatype="html">`, ` <context-group purpose="location">`,
` <source>pre<x id="START_TAG_SPAN" equiv-text="&apos;&lt;span&gt;&apos;"/>` + ` <context context-type="sourcefile">test_files/test.js</context>`,
`inner-pre<x id="START_BOLD_TEXT" equiv-text="&apos;&lt;b&gt;&apos;"/>bold<x id="CLOSE_BOLD_TEXT" equiv-text="&apos;&lt;/b&gt;&apos;"/>` + ` <context context-type="linenumber">8</context>`,
`inner-post<x id="CLOSE_TAG_SPAN" equiv-text="&apos;&lt;/span&gt;&apos;"/>post</source>`, ` </context-group>`,
` <context-group purpose="location">`, ` </trans-unit>`,
` <context context-type="sourcefile">test_files/test.js</context>`, ` <trans-unit id="2932901491976224757" datatype="html">`,
` <context context-type="linenumber">9,10</context>`, ` <source>pre<x id="START_TAG_SPAN" equiv-text="&apos;&lt;span&gt;&apos;"/>` +
` </context-group>`, `inner-pre<x id="START_BOLD_TEXT" equiv-text="&apos;&lt;b&gt;&apos;"/>bold<x id="CLOSE_BOLD_TEXT" equiv-text="&apos;&lt;/b&gt;&apos;"/>` +
` </trans-unit>`, `inner-post<x id="CLOSE_TAG_SPAN" equiv-text="&apos;&lt;/span&gt;&apos;"/>post</source>`,
` </body>`, ` <context-group purpose="location">`,
` </file>`, ` <context context-type="sourcefile">test_files/test.js</context>`,
`</xliff>\n`, ` <context context-type="linenumber">9,10</context>`,
].join('\n')); ` </context-group>`,
}); ` </trans-unit>`,
` </body>`,
` </file>`,
`</xliff>\n`,
].join('\n'));
});
it('should extract translations from source code, and write as XLIFF 2 format', () => { it('should extract translations from source code, and write as XLIFF 2 format', () => {
extractTranslations({ extractTranslations({
rootPath, rootPath,
sourceLocale: 'en-AU', sourceLocale: 'en-AU',
sourceFilePaths: [sourceFilePath], sourceFilePaths: [sourceFilePath],
format: 'xliff2', format: 'xliff2',
outputPath, outputPath,
logger, logger,
useSourceMaps: false, useSourceMaps: false,
useLegacyIds, useLegacyIds,
duplicateMessageHandling: 'ignore', duplicateMessageHandling: 'ignore',
formatOptions,
});
expect(fs.readFile(outputPath)).toEqual([
`<?xml version="1.0" encoding="UTF-8" ?>`,
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en-AU">`,
` <file id="ngi18n" original="ng.template"${toAttributes(formatOptions)}>`,
` <unit id="3291030485717846467">`,
` <notes>`,
` <note category="location">test_files/test.js:2</note>`,
` </notes>`,
` <segment>`,
` <source>Hello, <ph id="0" equiv="PH" disp="name"/>!</source>`,
` </segment>`,
` </unit>`,
` <unit id="8669027859022295761">`,
` <notes>`,
` <note category="location">test_files/test.js:3</note>`,
` </notes>`,
` <segment>`,
` <source>try<ph id="0" equiv="PH" disp="40 + 2"/>me</source>`,
` </segment>`,
` </unit>`,
` <unit id="custom-id">`,
` <notes>`,
` <note category="location">test_files/test.js:4</note>`,
` </notes>`,
` <segment>`,
` <source>Custom id message</source>`,
` </segment>`,
` </unit>`,
` <unit id="${useLegacyIds ? '12345678901234567890' : '273296103957933077'}">`,
` <notes>`,
` <note category="location">test_files/test.js:6</note>`,
` </notes>`,
` <segment>`,
` <source>Legacy id message</source>`,
` </segment>`,
` </unit>`,
` <unit id="custom-id-2">`,
` <notes>`,
` <note category="location">test_files/test.js:8</note>`,
` </notes>`,
` <segment>`,
` <source>Custom and legacy message</source>`,
` </segment>`,
` </unit>`,
` <unit id="2932901491976224757">`,
` <notes>`,
` <note category="location">test_files/test.js:9,10</note>`,
` </notes>`,
` <segment>`,
` <source>pre<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" dispStart="&apos;&lt;span&gt;&apos;" dispEnd="&apos;&lt;/span&gt;&apos;">` +
`inner-pre<pc id="1" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" dispStart="&apos;&lt;b&gt;&apos;" dispEnd="&apos;&lt;/b&gt;&apos;">bold</pc>` +
`inner-post</pc>post</source>`,
` </segment>`,
` </unit>`,
` </file>`,
`</xliff>\n`,
].join('\n'));
}); });
expect(fs.readFile(outputPath)).toEqual([ }
`<?xml version="1.0" encoding="UTF-8" ?>`,
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en-AU">`,
` <file id="ngi18n" original="ng.template">`,
` <unit id="3291030485717846467">`,
` <notes>`,
` <note category="location">test_files/test.js:2</note>`,
` </notes>`,
` <segment>`,
` <source>Hello, <ph id="0" equiv="PH" disp="name"/>!</source>`,
` </segment>`,
` </unit>`,
` <unit id="8669027859022295761">`,
` <notes>`,
` <note category="location">test_files/test.js:3</note>`,
` </notes>`,
` <segment>`,
` <source>try<ph id="0" equiv="PH" disp="40 + 2"/>me</source>`,
` </segment>`,
` </unit>`,
` <unit id="custom-id">`,
` <notes>`,
` <note category="location">test_files/test.js:4</note>`,
` </notes>`,
` <segment>`,
` <source>Custom id message</source>`,
` </segment>`,
` </unit>`,
` <unit id="${useLegacyIds ? '12345678901234567890' : '273296103957933077'}">`,
` <notes>`,
` <note category="location">test_files/test.js:6</note>`,
` </notes>`,
` <segment>`,
` <source>Legacy id message</source>`,
` </segment>`,
` </unit>`,
` <unit id="custom-id-2">`,
` <notes>`,
` <note category="location">test_files/test.js:8</note>`,
` </notes>`,
` <segment>`,
` <source>Custom and legacy message</source>`,
` </segment>`,
` </unit>`,
` <unit id="2932901491976224757">`,
` <notes>`,
` <note category="location">test_files/test.js:9,10</note>`,
` </notes>`,
` <segment>`,
` <source>pre<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" dispStart="&apos;&lt;span&gt;&apos;" dispEnd="&apos;&lt;/span&gt;&apos;">` +
`inner-pre<pc id="1" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" dispStart="&apos;&lt;b&gt;&apos;" dispEnd="&apos;&lt;/b&gt;&apos;">bold</pc>` +
`inner-post</pc>post</source>`,
` </segment>`,
` </unit>`,
` </file>`,
`</xliff>\n`,
].join('\n'));
});
}); });
} }

View File

@ -0,0 +1,49 @@
/**
* @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 {parseFormatOptions, validateOptions} from '../../../src/extract/translation_files/format_options';
describe('format_options', () => {
describe('validateOptions()', () => {
it('should do nothing if there are no options', () => {
expect(() => validateOptions('TestSerializer', [['key', ['value1', 'value2']]], {}))
.not.toThrow();
});
it('should do nothing if the options are valid', () => {
expect(
() => validateOptions('TestSerializer', [['key', ['value1', 'value2']]], {key: 'value1'}))
.not.toThrow();
});
it('should error if there is an unexpected option', () => {
expect(
() => validateOptions('TestSerializer', [['key', ['value1', 'value2']]], {wrong: 'xxx'}))
.toThrowError(
'Invalid format option for TestSerializer: "wrong".\n' +
'Allowed options are ["key"].');
});
it('should error if there is an unexpected option value', () => {
expect(
() => validateOptions('TestSerializer', [['key', ['value1', 'value2']]], {key: 'other'}))
.toThrowError(
'Invalid format option value for TestSerializer: "key".\n' +
'Allowed option values are ["value1","value2"] but received "other".');
});
});
describe('parseFormatOptions()', () => {
it('should parse the string as JSON', () => {
expect(parseFormatOptions('{"a": "1", "b": "2"}')).toEqual({a: '1', b: '2'});
});
it('should parse undefined into an empty object', () => {
expect(parseFormatOptions(undefined)).toEqual({});
});
});
});

View File

@ -0,0 +1,17 @@
/**
* @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 {FormatOptions} from '../../../src/extract/translation_files/format_options';
export function toAttributes(options: FormatOptions) {
let result = '';
for (const option in options) {
result += ` ${option}="${options[option]}"`;
}
return result;
}

View File

@ -9,127 +9,132 @@ import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; 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 {Xliff1TranslationSerializer} from '../../../src/extract/translation_files/xliff1_translation_serializer';
import {mockMessage} from './mock_message'; import {mockMessage} from './mock_message';
import {toAttributes} from './utils';
runInEachFileSystem(() => { runInEachFileSystem(() => {
describe('Xliff1TranslationSerializer', () => { describe('Xliff1TranslationSerializer', () => {
[false, true].forEach(useLegacyIds => { ([{}, {'xml:space': 'preserve'}] as FormatOptions[]).forEach(options => {
describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => { [false, true].forEach(useLegacyIds => {
it('should convert a set of parsed messages into an XML string', () => { describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => {
const phLocation: ɵSourceLocation = { it('should convert a set of parsed messages into an XML string', () => {
start: {line: 0, column: 10}, const phLocation: ɵSourceLocation = {
end: {line: 1, column: 15}, start: {line: 0, column: 10},
file: absoluteFrom('/project/file.ts'), end: {line: 1, column: 15},
text: 'placeholder + 1' file: absoluteFrom('/project/file.ts'),
}; text: 'placeholder + 1'
const messagePartLocation: ɵSourceLocation = { };
start: {line: 0, column: 5}, const messagePartLocation: ɵSourceLocation = {
end: {line: 0, column: 10}, start: {line: 0, column: 5},
file: absoluteFrom('/project/file.ts'), end: {line: 0, column: 10},
text: 'message part' file: absoluteFrom('/project/file.ts'),
}; text: 'message part'
const messages: ɵParsedMessage[] = [ };
mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], { const messages: ɵParsedMessage[] = [
meaning: 'some meaning', mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], {
location: { meaning: 'some meaning',
file: absoluteFrom('/project/file.ts'), location: {
start: {line: 5, column: 10}, file: absoluteFrom('/project/file.ts'),
end: {line: 5, column: 12} start: {line: 5, column: 10},
}, end: {line: 5, column: 12}
legacyIds: ['1234567890ABCDEF1234567890ABCDEF12345678', '615790887472569365'], },
}), legacyIds: ['1234567890ABCDEF1234567890ABCDEF12345678', '615790887472569365'],
mockMessage('54321', ['a', 'b', 'c'], ['PH', 'PH_1'], { }),
customId: 'someId', mockMessage('54321', ['a', 'b', 'c'], ['PH', 'PH_1'], {
legacyIds: ['87654321FEDCBA0987654321FEDCBA0987654321', '563965274788097516'], customId: 'someId',
messagePartLocations: [undefined, messagePartLocation, undefined], legacyIds: ['87654321FEDCBA0987654321FEDCBA0987654321', '563965274788097516'],
substitutionLocations: {'PH': phLocation, 'PH_1': undefined}, messagePartLocations: [undefined, messagePartLocation, undefined],
}), substitutionLocations: {'PH': phLocation, 'PH_1': undefined},
mockMessage( }),
'67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], mockMessage(
{description: 'some description'}), '67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'],
mockMessage('38705', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], { {description: 'some description'}),
location: { mockMessage('38705', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {
file: absoluteFrom('/project/file.ts'), location: {
start: {line: 2, column: 7}, file: absoluteFrom('/project/file.ts'),
end: {line: 3, column: 2} start: {line: 2, column: 7},
} end: {line: 3, column: 2}
}), }
mockMessage('13579', ['', 'b', ''], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {}), }),
mockMessage('24680', ['a'], [], {meaning: 'meaning', description: 'and description'}), mockMessage('13579', ['', 'b', ''], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {}),
mockMessage('80808', ['multi\nlines'], [], {}), mockMessage('24680', ['a'], [], {meaning: 'meaning', description: 'and description'}),
mockMessage('90000', ['<escape', 'me>'], ['double-quotes-"'], {}), mockMessage('80808', ['multi\nlines'], [], {}),
mockMessage( mockMessage('90000', ['<escape', 'me>'], ['double-quotes-"'], {}),
'100000', mockMessage(
[ '100000',
'pre-ICU {VAR_SELECT, select, a {a} b {{INTERPOLATION}} c {pre {INTERPOLATION_1} post}} post-ICU' [
], 'pre-ICU {VAR_SELECT, select, a {a} b {{INTERPOLATION}} c {pre {INTERPOLATION_1} post}} post-ICU'
[], {}), ],
mockMessage( [], {}),
'100001', mockMessage(
[ '100001',
'{VAR_PLURAL, plural, one {{START_BOLD_TEXT}something bold{CLOSE_BOLD_TEXT}} other {pre {START_TAG_SPAN}middle{CLOSE_TAG_SPAN} post}}' [
], '{VAR_PLURAL, plural, one {{START_BOLD_TEXT}something bold{CLOSE_BOLD_TEXT}} other {pre {START_TAG_SPAN}middle{CLOSE_TAG_SPAN} post}}'
[], {}), ],
]; [], {}),
const serializer = ];
new Xliff1TranslationSerializer('xx', absoluteFrom('/project'), useLegacyIds); const serializer = new Xliff1TranslationSerializer(
const output = serializer.serialize(messages); 'xx', absoluteFrom('/project'), useLegacyIds, options);
expect(output).toEqual([ const output = serializer.serialize(messages);
`<?xml version="1.0" encoding="UTF-8" ?>`, expect(output).toEqual([
`<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">`, `<?xml version="1.0" encoding="UTF-8" ?>`,
` <file source-language="xx" datatype="plaintext" original="ng2.template">`, `<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">`,
` <body>`, ` <file source-language="xx" datatype="plaintext" original="ng2.template"${
` <trans-unit id="${ toAttributes(options)}>`,
useLegacyIds ? '1234567890ABCDEF1234567890ABCDEF12345678' : ` <body>`,
'12345'}" datatype="html">`, ` <trans-unit id="${
` <source>a<x id="PH"/>b<x id="PH_1"/>c</source>`, useLegacyIds ? '1234567890ABCDEF1234567890ABCDEF12345678' :
` <context-group purpose="location">`, '12345'}" datatype="html">`,
` <context context-type="sourcefile">file.ts</context>`, ` <source>a<x id="PH"/>b<x id="PH_1"/>c</source>`,
` <context context-type="linenumber">6</context>`, ` <context-group purpose="location">`,
` </context-group>`, ` <context context-type="sourcefile">file.ts</context>`,
` <note priority="1" from="meaning">some meaning</note>`, ` <context context-type="linenumber">6</context>`,
` </trans-unit>`, ` </context-group>`,
` <trans-unit id="someId" datatype="html">`, ` <note priority="1" from="meaning">some meaning</note>`,
` <source>a<x id="PH" equiv-text="placeholder + 1"/>b<x id="PH_1"/>c</source>`, ` </trans-unit>`,
` </trans-unit>`, ` <trans-unit id="someId" datatype="html">`,
` <trans-unit id="67890" datatype="html">`, ` <source>a<x id="PH" equiv-text="placeholder + 1"/>b<x id="PH_1"/>c</source>`,
` <source>a<x id="START_TAG_SPAN"/><x id="CLOSE_TAG_SPAN"/>c</source>`, ` </trans-unit>`,
` <note priority="1" from="description">some description</note>`, ` <trans-unit id="67890" datatype="html">`,
` </trans-unit>`, ` <source>a<x id="START_TAG_SPAN"/><x id="CLOSE_TAG_SPAN"/>c</source>`,
` <trans-unit id="38705" datatype="html">`, ` <note priority="1" from="description">some description</note>`,
` <source>a<x id="START_TAG_SPAN"/><x id="CLOSE_TAG_SPAN"/>c</source>`, ` </trans-unit>`,
` <context-group purpose="location">`, ` <trans-unit id="38705" datatype="html">`,
` <context context-type="sourcefile">file.ts</context>`, ` <source>a<x id="START_TAG_SPAN"/><x id="CLOSE_TAG_SPAN"/>c</source>`,
` <context context-type="linenumber">3,4</context>`, ` <context-group purpose="location">`,
` </context-group>`, ` <context context-type="sourcefile">file.ts</context>`,
` </trans-unit>`, ` <context context-type="linenumber">3,4</context>`,
` <trans-unit id="13579" datatype="html">`, ` </context-group>`,
` <source><x id="START_BOLD_TEXT"/>b<x id="CLOSE_BOLD_TEXT"/></source>`, ` </trans-unit>`,
` </trans-unit>`, ` <trans-unit id="13579" datatype="html">`,
` <trans-unit id="24680" datatype="html">`, ` <source><x id="START_BOLD_TEXT"/>b<x id="CLOSE_BOLD_TEXT"/></source>`,
` <source>a</source>`, ` </trans-unit>`,
` <note priority="1" from="description">and description</note>`, ` <trans-unit id="24680" datatype="html">`,
` <note priority="1" from="meaning">meaning</note>`, ` <source>a</source>`,
` </trans-unit>`, ` <note priority="1" from="description">and description</note>`,
` <trans-unit id="80808" datatype="html">`, ` <note priority="1" from="meaning">meaning</note>`,
` <source>multi`, ` </trans-unit>`,
`lines</source>`, ` <trans-unit id="80808" datatype="html">`,
` </trans-unit>`, ` <source>multi`,
` <trans-unit id="90000" datatype="html">`, `lines</source>`,
` <source>&lt;escape<x id="double-quotes-&quot;"/>me&gt;</source>`, ` </trans-unit>`,
` </trans-unit>`, ` <trans-unit id="90000" datatype="html">`,
` <trans-unit id="100000" datatype="html">`, ` <source>&lt;escape<x id="double-quotes-&quot;"/>me&gt;</source>`,
` <source>pre-ICU {VAR_SELECT, select, a {a} b {<x id="INTERPOLATION"/>} c {pre <x id="INTERPOLATION_1"/> post}} post-ICU</source>`, ` </trans-unit>`,
` </trans-unit>`, ` <trans-unit id="100000" datatype="html">`,
` <trans-unit id="100001" datatype="html">`, ` <source>pre-ICU {VAR_SELECT, select, a {a} b {<x id="INTERPOLATION"/>} c {pre <x id="INTERPOLATION_1"/> post}} post-ICU</source>`,
` <source>{VAR_PLURAL, plural, one {<x id="START_BOLD_TEXT"/>something bold<x id="CLOSE_BOLD_TEXT"/>} other {pre <x id="START_TAG_SPAN"/>middle<x id="CLOSE_TAG_SPAN"/> post}}</source>`, ` </trans-unit>`,
` </trans-unit>`, ` <trans-unit id="100001" datatype="html">`,
` </body>`, ` <source>{VAR_PLURAL, plural, one {<x id="START_BOLD_TEXT"/>something bold<x id="CLOSE_BOLD_TEXT"/>} other {pre <x id="START_TAG_SPAN"/>middle<x id="CLOSE_TAG_SPAN"/> post}}</source>`,
` </file>`, ` </trans-unit>`,
`</xliff>\n`, ` </body>`,
].join('\n')); ` </file>`,
`</xliff>\n`,
].join('\n'));
});
}); });
}); });
}); });

View File

@ -9,151 +9,155 @@ import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; 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 {Xliff2TranslationSerializer} from '../../../src/extract/translation_files/xliff2_translation_serializer';
import {mockMessage} from './mock_message'; import {mockMessage} from './mock_message';
import {toAttributes} from './utils';
runInEachFileSystem(() => { runInEachFileSystem(() => {
describe('Xliff2TranslationSerializer', () => { describe('Xliff2TranslationSerializer', () => {
[false, true].forEach(useLegacyIds => { ([{}, {'xml:space': 'preserve'}] as FormatOptions[]).forEach(options => {
describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => { [false, true].forEach(useLegacyIds => {
it('should convert a set of parsed messages into an XML string', () => { describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => {
const phLocation: ɵSourceLocation = { it('should convert a set of parsed messages into an XML string', () => {
start: {line: 0, column: 10}, const phLocation: ɵSourceLocation = {
end: {line: 1, column: 15}, start: {line: 0, column: 10},
file: absoluteFrom('/project/file.ts'), end: {line: 1, column: 15},
text: 'placeholder + 1' file: absoluteFrom('/project/file.ts'),
}; text: 'placeholder + 1'
const messagePartLocation: ɵSourceLocation = { };
start: {line: 0, column: 5}, const messagePartLocation: ɵSourceLocation = {
end: {line: 0, column: 10}, start: {line: 0, column: 5},
file: absoluteFrom('/project/file.ts'), end: {line: 0, column: 10},
text: 'message part' file: absoluteFrom('/project/file.ts'),
}; text: 'message part'
const messages: ɵParsedMessage[] = [ };
mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], { const messages: ɵParsedMessage[] = [
meaning: 'some meaning', mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], {
location: { meaning: 'some meaning',
file: absoluteFrom('/project/file.ts'), location: {
start: {line: 5, column: 0}, file: absoluteFrom('/project/file.ts'),
end: {line: 5, column: 3} start: {line: 5, column: 0},
}, end: {line: 5, column: 3}
legacyIds: ['1234567890ABCDEF1234567890ABCDEF12345678', '615790887472569365'], },
}), legacyIds: ['1234567890ABCDEF1234567890ABCDEF12345678', '615790887472569365'],
mockMessage('54321', ['a', 'b', 'c'], ['PH', 'PH_1'], { }),
customId: 'someId', mockMessage('54321', ['a', 'b', 'c'], ['PH', 'PH_1'], {
legacyIds: ['87654321FEDCBA0987654321FEDCBA0987654321', '563965274788097516'], customId: 'someId',
messagePartLocations: [undefined, messagePartLocation, undefined], legacyIds: ['87654321FEDCBA0987654321FEDCBA0987654321', '563965274788097516'],
substitutionLocations: {'PH': phLocation, 'PH_1': undefined}, messagePartLocations: [undefined, messagePartLocation, undefined],
}), substitutionLocations: {'PH': phLocation, 'PH_1': undefined},
mockMessage('67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], { }),
description: 'some description', mockMessage('67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {
location: { description: 'some description',
file: absoluteFrom('/project/file.ts'), location: {
start: {line: 2, column: 7}, file: absoluteFrom('/project/file.ts'),
end: {line: 3, column: 2} start: {line: 2, column: 7},
} end: {line: 3, column: 2}
}), }
mockMessage('location-only', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], { }),
location: { mockMessage('location-only', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {
file: absoluteFrom('/project/file.ts'), location: {
start: {line: 2, column: 7}, file: absoluteFrom('/project/file.ts'),
end: {line: 3, column: 2} start: {line: 2, column: 7},
} end: {line: 3, column: 2}
}), }
mockMessage('13579', ['', 'b', ''], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {}), }),
mockMessage('24680', ['a'], [], {meaning: 'meaning', description: 'and description'}), mockMessage('13579', ['', 'b', ''], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {}),
mockMessage('80808', ['multi\nlines'], [], {}), mockMessage('24680', ['a'], [], {meaning: 'meaning', description: 'and description'}),
mockMessage('90000', ['<escape', 'me>'], ['double-quotes-"'], {}), mockMessage('80808', ['multi\nlines'], [], {}),
mockMessage( mockMessage('90000', ['<escape', 'me>'], ['double-quotes-"'], {}),
'100000', mockMessage(
[ '100000',
'pre-ICU {VAR_SELECT, select, a {a} b {{INTERPOLATION}} c {pre {INTERPOLATION_1} post}} post-ICU' [
], 'pre-ICU {VAR_SELECT, select, a {a} b {{INTERPOLATION}} c {pre {INTERPOLATION_1} post}} post-ICU'
[], {}), ],
mockMessage( [], {}),
'100001', mockMessage(
[ '100001',
'{VAR_PLURAL, plural, one {{START_BOLD_TEXT}something bold{CLOSE_BOLD_TEXT}} other {pre {START_TAG_SPAN}middle{CLOSE_TAG_SPAN} post}}' [
], '{VAR_PLURAL, plural, one {{START_BOLD_TEXT}something bold{CLOSE_BOLD_TEXT}} other {pre {START_TAG_SPAN}middle{CLOSE_TAG_SPAN} post}}'
[], {}), ],
]; [], {}),
const serializer = ];
new Xliff2TranslationSerializer('xx', absoluteFrom('/project'), useLegacyIds); const serializer = new Xliff2TranslationSerializer(
const output = serializer.serialize(messages); 'xx', absoluteFrom('/project'), useLegacyIds, options);
expect(output).toEqual([ const output = serializer.serialize(messages);
`<?xml version="1.0" encoding="UTF-8" ?>`, expect(output).toEqual([
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="xx">`, `<?xml version="1.0" encoding="UTF-8" ?>`,
` <file id="ngi18n" original="ng.template">`, `<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="xx">`,
` <unit id="${useLegacyIds ? '615790887472569365' : '12345'}">`, ` <file id="ngi18n" original="ng.template"${toAttributes(options)}>`,
` <notes>`, ` <unit id="${useLegacyIds ? '615790887472569365' : '12345'}">`,
` <note category="location">file.ts:6</note>`, ` <notes>`,
` <note category="meaning">some meaning</note>`, ` <note category="location">file.ts:6</note>`,
` </notes>`, ` <note category="meaning">some meaning</note>`,
` <segment>`, ` </notes>`,
` <source>a<ph id="0" equiv="PH"/>b<ph id="1" equiv="PH_1"/>c</source>`, ` <segment>`,
` </segment>`, ` <source>a<ph id="0" equiv="PH"/>b<ph id="1" equiv="PH_1"/>c</source>`,
` </unit>`, ` </segment>`,
` <unit id="someId">`, ` </unit>`,
` <segment>`, ` <unit id="someId">`,
` <source>a<ph id="0" equiv="PH" disp="placeholder + 1"/>b<ph id="1" equiv="PH_1"/>c</source>`, ` <segment>`,
` </segment>`, ` <source>a<ph id="0" equiv="PH" disp="placeholder + 1"/>b<ph id="1" equiv="PH_1"/>c</source>`,
` </unit>`, ` </segment>`,
` <unit id="67890">`, ` </unit>`,
` <notes>`, ` <unit id="67890">`,
` <note category="location">file.ts:3,4</note>`, ` <notes>`,
` <note category="description">some description</note>`, ` <note category="location">file.ts:3,4</note>`,
` </notes>`, ` <note category="description">some description</note>`,
` <segment>`, ` </notes>`,
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN"></pc>c</source>`, ` <segment>`,
` </segment>`, ` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN"></pc>c</source>`,
` </unit>`, ` </segment>`,
` <unit id="location-only">`, ` </unit>`,
` <notes>`, ` <unit id="location-only">`,
` <note category="location">file.ts:3,4</note>`, ` <notes>`,
` </notes>`, ` <note category="location">file.ts:3,4</note>`,
` <segment>`, ` </notes>`,
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN"></pc>c</source>`, ` <segment>`,
` </segment>`, ` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN"></pc>c</source>`,
` </unit>`, ` </segment>`,
` <unit id="13579">`, ` </unit>`,
` <segment>`, ` <unit id="13579">`,
` <source><pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT">b</pc></source>`, ` <segment>`,
` </segment>`, ` <source><pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT">b</pc></source>`,
` </unit>`, ` </segment>`,
` <unit id="24680">`, ` </unit>`,
` <notes>`, ` <unit id="24680">`,
` <note category="description">and description</note>`, ` <notes>`,
` <note category="meaning">meaning</note>`, ` <note category="description">and description</note>`,
` </notes>`, ` <note category="meaning">meaning</note>`,
` <segment>`, ` </notes>`,
` <source>a</source>`, ` <segment>`,
` </segment>`, ` <source>a</source>`,
` </unit>`, ` </segment>`,
` <unit id="80808">`, ` </unit>`,
` <segment>`, ` <unit id="80808">`,
` <source>multi`, ` <segment>`,
`lines</source>`, ` <source>multi`,
` </segment>`, `lines</source>`,
` </unit>`, ` </segment>`,
` <unit id="90000">`, ` </unit>`,
` <segment>`, ` <unit id="90000">`,
` <source>&lt;escape<ph id="0" equiv="double-quotes-&quot;"/>me&gt;</source>`, ` <segment>`,
` </segment>`, ` <source>&lt;escape<ph id="0" equiv="double-quotes-&quot;"/>me&gt;</source>`,
` </unit>`, ` </segment>`,
` <unit id="100000">`, ` </unit>`,
` <segment>`, ` <unit id="100000">`,
` <source>pre-ICU {VAR_SELECT, select, a {a} b {<ph id="0" equiv="INTERPOLATION"/>} c {pre <ph id="1" equiv="INTERPOLATION_1"/> post}} post-ICU</source>`, ` <segment>`,
` </segment>`, ` <source>pre-ICU {VAR_SELECT, select, a {a} b {<ph id="0" equiv="INTERPOLATION"/>} c {pre <ph id="1" equiv="INTERPOLATION_1"/> post}} post-ICU</source>`,
` </unit>`, ` </segment>`,
` <unit id="100001">`, ` </unit>`,
` <segment>`, ` <unit id="100001">`,
` <source>{VAR_PLURAL, plural, one {<pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT">something bold</pc>} other {pre <pc id="1" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN">middle</pc> post}}</source>`, ` <segment>`,
` </segment>`, ` <source>{VAR_PLURAL, plural, one {<pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT">something bold</pc>} other {pre <pc id="1" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN">middle</pc> post}}</source>`,
` </unit>`, ` </segment>`,
` </file>`, ` </unit>`,
`</xliff>\n`, ` </file>`,
].join('\n')); `</xliff>\n`,
].join('\n'));
});
}); });
}); });
}); });