feat(localize): implement message extraction tool (#32912)
This tool, which can be run from the node_modules bin folder, can parse the source files in your compiled app and generate a translation file formatted with the configured syntax. For example: ``` ./node_modules/.bin/localize-extract -s 'dist/**/*.js' -f xliff1 -o dist/messages.en.xlf ``` PR Close #32912
This commit is contained in:
parent
ddb0a4e2e5
commit
190561d8a6
|
@ -3,7 +3,8 @@
|
|||
"version": "0.0.0-PLACEHOLDER",
|
||||
"description": "Angular - library for localizing messages",
|
||||
"bin": {
|
||||
"localize-translate": "./src/tools/src/translate/main.js"
|
||||
"localize-translate": "./src/tools/src/translate/main.js",
|
||||
"localize-extract": "./src/tools/src/extract/main.js"
|
||||
},
|
||||
"author": "angular",
|
||||
"license": "MIT",
|
||||
|
|
|
@ -20,6 +20,8 @@ ts_library(
|
|||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/logging",
|
||||
"//packages/compiler-cli/src/ngtsc/sourcemaps",
|
||||
"//packages/localize",
|
||||
"@npm//@babel/core",
|
||||
"@npm//@babel/types",
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* @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, FileSystem, PathSegment} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {Logger} from '@angular/compiler-cli/src/ngtsc/logging';
|
||||
import {SourceFile, SourceFileLoader} from '@angular/compiler-cli/src/ngtsc/sourcemaps';
|
||||
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
|
||||
import {transformSync} from '@babel/core';
|
||||
|
||||
import {makeEs2015ExtractPlugin} from './source_files/es2015_extract_plugin';
|
||||
import {makeEs5ExtractPlugin} from './source_files/es5_extract_plugin';
|
||||
|
||||
export interface ExtractionOptions {
|
||||
basePath: AbsoluteFsPath;
|
||||
useSourceMaps?: boolean;
|
||||
localizeName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts parsed messages from file contents, by parsing the contents as JavaScript
|
||||
* and looking for occurrences of `$localize` in the source code.
|
||||
*/
|
||||
export class MessageExtractor {
|
||||
private basePath: AbsoluteFsPath;
|
||||
private useSourceMaps: boolean;
|
||||
private localizeName: string;
|
||||
private loader: SourceFileLoader;
|
||||
|
||||
constructor(
|
||||
private fs: FileSystem, private logger: Logger,
|
||||
{basePath, useSourceMaps = true, localizeName = '$localize'}: ExtractionOptions) {
|
||||
this.basePath = basePath;
|
||||
this.useSourceMaps = useSourceMaps;
|
||||
this.localizeName = localizeName;
|
||||
this.loader = new SourceFileLoader(this.fs, this.logger, {webpack: basePath});
|
||||
}
|
||||
|
||||
extractMessages(
|
||||
filename: string,
|
||||
): ɵParsedMessage[] {
|
||||
const messages: ɵParsedMessage[] = [];
|
||||
const sourceCode = this.fs.readFile(this.fs.resolve(this.basePath, filename));
|
||||
if (sourceCode.includes(this.localizeName)) {
|
||||
// Only bother to parse the file if it contains a reference to `$localize`.
|
||||
transformSync(sourceCode, {
|
||||
sourceRoot: this.basePath,
|
||||
filename,
|
||||
plugins: [
|
||||
makeEs2015ExtractPlugin(messages, this.localizeName),
|
||||
makeEs5ExtractPlugin(messages, this.localizeName),
|
||||
],
|
||||
code: false,
|
||||
ast: false
|
||||
});
|
||||
}
|
||||
if (this.useSourceMaps) {
|
||||
this.updateSourceLocations(filename, sourceCode, messages);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the location of each message to point to the source-mapped original source location, if
|
||||
* available.
|
||||
*/
|
||||
private updateSourceLocations(filename: string, contents: string, messages: ɵParsedMessage[]):
|
||||
void {
|
||||
const sourceFile =
|
||||
this.loader.loadSourceFile(this.fs.resolve(this.basePath, filename), contents);
|
||||
if (sourceFile === null) {
|
||||
return;
|
||||
}
|
||||
for (const message of messages) {
|
||||
if (message.location !== undefined) {
|
||||
message.location = this.getOriginalLocation(sourceFile, message.location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the original location using source-maps if available.
|
||||
*
|
||||
* @param sourceFile The generated `sourceFile` that contains the `location`.
|
||||
* @param location The location within the generated `sourceFile` that needs mapping.
|
||||
*
|
||||
* @returns A new location that refers to the original source location mapped from the given
|
||||
* `location` in the generated `sourceFile`.
|
||||
*/
|
||||
private getOriginalLocation(sourceFile: SourceFile, location: ɵSourceLocation): ɵSourceLocation {
|
||||
const originalStart =
|
||||
sourceFile.getOriginalLocation(location.start.line, location.start.column);
|
||||
if (originalStart === null) {
|
||||
return location;
|
||||
}
|
||||
const originalEnd = sourceFile.getOriginalLocation(location.end.line, location.end.column);
|
||||
const start = {line: originalStart.line, column: originalStart.column};
|
||||
// We check whether the files are the same, since the returned location can only have a single
|
||||
// `file` and it would not make sense to store the end position from a different source file.
|
||||
const end = (originalEnd !== null && originalEnd.file === originalStart.file) ?
|
||||
{line: originalEnd.line, column: originalEnd.column} :
|
||||
start;
|
||||
return {file: originalStart.file, start, end};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* @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 {getFileSystem, setFileSystem, NodeJSFileSystem, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {ConsoleLogger, Logger, LogLevel} from '@angular/compiler-cli/src/ngtsc/logging';
|
||||
import {ɵParsedMessage} from '@angular/localize';
|
||||
import * as glob from 'glob';
|
||||
import * as yargs from 'yargs';
|
||||
import {MessageExtractor} from './extraction';
|
||||
import {TranslationSerializer} from './translation_files/translation_serializer';
|
||||
import {SimpleJsonTranslationSerializer} from './translation_files/json_translation_serializer';
|
||||
import {Xliff1TranslationSerializer} from './translation_files/xliff1_translation_serializer';
|
||||
import {Xliff2TranslationSerializer} from './translation_files/xliff2_translation_serializer';
|
||||
import {XmbTranslationSerializer} from './translation_files/xmb_translation_serializer';
|
||||
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const options =
|
||||
yargs
|
||||
.option('l', {
|
||||
alias: 'locale',
|
||||
describe: 'The locale of the source being processed',
|
||||
default: 'en',
|
||||
})
|
||||
.option('r', {
|
||||
alias: 'root',
|
||||
default: '.',
|
||||
describe: 'The root path for other paths provided in these options.\n' +
|
||||
'This should either be absolute or relative to the current working directory.'
|
||||
})
|
||||
.option('s', {
|
||||
alias: 'source',
|
||||
required: true,
|
||||
describe:
|
||||
'A glob pattern indicating what files to search for translations, e.g. `./dist/**/*.js`.\n' +
|
||||
'This should be relative to the root path.',
|
||||
})
|
||||
.option('f', {
|
||||
alias: 'format',
|
||||
required: true,
|
||||
choices: ['xmb', 'xlf', 'xlif', 'xliff', 'xlf2', 'xlif2', 'xliff2', 'json'],
|
||||
describe: 'The format of the translation file.',
|
||||
})
|
||||
.option('o', {
|
||||
alias: 'outputPath',
|
||||
required: true,
|
||||
describe:
|
||||
'A path to where the translation file will be written. This should be relative to the root path.'
|
||||
})
|
||||
.option('loglevel', {
|
||||
describe: 'The lowest severity logging message that should be output.',
|
||||
choices: ['debug', 'info', 'warn', 'error'],
|
||||
})
|
||||
.option('useSourceMaps', {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
describe:
|
||||
'Whether to generate source information in the output files by following source-map mappings found in the source files'
|
||||
})
|
||||
.option('useLegacyIds', {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
describe:
|
||||
'Whether to use the legacy id format for messages that were extracted from Angular templates.'
|
||||
})
|
||||
.strict()
|
||||
.help()
|
||||
.parse(args);
|
||||
|
||||
const fs = new NodeJSFileSystem();
|
||||
setFileSystem(fs);
|
||||
|
||||
const rootPath = options['root'];
|
||||
const sourceFilePaths = glob.sync(options['source'], {cwd: rootPath, nodir: true});
|
||||
const logLevel = options['loglevel'] as (keyof typeof LogLevel) | undefined;
|
||||
const logger = new ConsoleLogger(logLevel ? LogLevel[logLevel] : LogLevel.warn);
|
||||
|
||||
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceFilePaths,
|
||||
sourceLocale: options['locale'],
|
||||
format: options['format'],
|
||||
outputPath: options['outputPath'],
|
||||
logger,
|
||||
useSourceMaps: options['useSourceMaps'],
|
||||
useLegacyIds: options['useLegacyIds'],
|
||||
});
|
||||
}
|
||||
|
||||
export interface ExtractTranslationsOptions {
|
||||
/**
|
||||
* The locale of the source being processed.
|
||||
*/
|
||||
sourceLocale: string;
|
||||
/**
|
||||
* The base path for other paths provided in these options.
|
||||
* This should either be absolute or relative to the current working directory.
|
||||
*/
|
||||
rootPath: string;
|
||||
/**
|
||||
* An array of paths to files to search for translations. These should be relative to the
|
||||
* rootPath.
|
||||
*/
|
||||
sourceFilePaths: string[];
|
||||
/**
|
||||
* The format of the translation file.
|
||||
*/
|
||||
format: string;
|
||||
/**
|
||||
* A path to where the translation file will be written. This should be relative to the rootPath.
|
||||
*/
|
||||
outputPath: string;
|
||||
/**
|
||||
* The logger to use for diagnostic messages.
|
||||
*/
|
||||
logger: Logger;
|
||||
/**
|
||||
* Whether to generate source information in the output files by following source-map mappings
|
||||
* found in the source file.
|
||||
*/
|
||||
useSourceMaps: boolean;
|
||||
/**
|
||||
* Whether to use the legacy id format for messages that were extracted from Angular templates
|
||||
*/
|
||||
useLegacyIds: boolean;
|
||||
}
|
||||
|
||||
export function extractTranslations({
|
||||
rootPath,
|
||||
sourceFilePaths,
|
||||
sourceLocale,
|
||||
format,
|
||||
outputPath: output,
|
||||
logger,
|
||||
useSourceMaps,
|
||||
useLegacyIds
|
||||
}: ExtractTranslationsOptions) {
|
||||
const fs = getFileSystem();
|
||||
const extractor =
|
||||
new MessageExtractor(fs, logger, {basePath: fs.resolve(rootPath), useSourceMaps});
|
||||
|
||||
const messages: ɵParsedMessage[] = [];
|
||||
for (const file of sourceFilePaths) {
|
||||
messages.push(...extractor.extractMessages(file));
|
||||
}
|
||||
|
||||
const outputPath = fs.resolve(rootPath, output);
|
||||
const serializer = getSerializer(format, sourceLocale, fs.dirname(outputPath), useLegacyIds);
|
||||
const translationFile = serializer.serialize(messages);
|
||||
fs.ensureDir(fs.dirname(outputPath));
|
||||
fs.writeFile(outputPath, translationFile);
|
||||
}
|
||||
|
||||
export function getSerializer(
|
||||
format: string, sourceLocale: string, rootPath: AbsoluteFsPath,
|
||||
useLegacyIds: boolean): TranslationSerializer {
|
||||
switch (format) {
|
||||
case 'xlf':
|
||||
case 'xlif':
|
||||
case 'xliff':
|
||||
return new Xliff1TranslationSerializer(sourceLocale, rootPath, useLegacyIds);
|
||||
case 'xlf2':
|
||||
case 'xlif2':
|
||||
case 'xliff2':
|
||||
return new Xliff2TranslationSerializer(sourceLocale, rootPath, useLegacyIds);
|
||||
case 'xmb':
|
||||
return new XmbTranslationSerializer(rootPath, useLegacyIds);
|
||||
case 'json':
|
||||
return new SimpleJsonTranslationSerializer(sourceLocale);
|
||||
}
|
||||
throw new Error(`No translation serializer can handle the provided format: ${format}`);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* @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 {ɵParsedMessage, ɵparseMessage} from '@angular/localize';
|
||||
import {NodePath, PluginObj} from '@babel/core';
|
||||
import {TaggedTemplateExpression} from '@babel/types';
|
||||
|
||||
import {getLocation, isGlobalIdentifier, isNamedIdentifier, unwrapMessagePartsFromTemplateLiteral} from '../../source_file_utils';
|
||||
|
||||
export function makeEs2015ExtractPlugin(
|
||||
messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj {
|
||||
return {
|
||||
visitor: {
|
||||
TaggedTemplateExpression(path: NodePath<TaggedTemplateExpression>) {
|
||||
const tag = path.get('tag');
|
||||
if (isNamedIdentifier(tag, localizeName) && isGlobalIdentifier(tag)) {
|
||||
const messageParts = unwrapMessagePartsFromTemplateLiteral(path.node.quasi.quasis);
|
||||
const location = getLocation(path.get('quasi'));
|
||||
const message = ɵparseMessage(messageParts, path.node.quasi.expressions, location);
|
||||
messages.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @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 {ɵParsedMessage, ɵparseMessage} from '@angular/localize';
|
||||
import {NodePath, PluginObj} from '@babel/core';
|
||||
import {CallExpression} from '@babel/types';
|
||||
|
||||
import {getLocation, isGlobalIdentifier, isNamedIdentifier, unwrapMessagePartsFromLocalizeCall, unwrapSubstitutionsFromLocalizeCall} from '../../source_file_utils';
|
||||
|
||||
export function makeEs5ExtractPlugin(
|
||||
messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj {
|
||||
return {
|
||||
visitor: {
|
||||
CallExpression(callPath: NodePath<CallExpression>) {
|
||||
const calleePath = callPath.get('callee');
|
||||
if (isNamedIdentifier(calleePath, localizeName) && isGlobalIdentifier(calleePath)) {
|
||||
const messageParts = unwrapMessagePartsFromLocalizeCall(callPath);
|
||||
const expressions = unwrapSubstitutionsFromLocalizeCall(callPath.node);
|
||||
const location = getLocation(callPath);
|
||||
const message = ɵparseMessage(messageParts, expressions, location);
|
||||
messages.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @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 {ɵMessageId, ɵParsedMessage, ɵSourceMessage} from '@angular/localize';
|
||||
import {TranslationSerializer} from './translation_serializer';
|
||||
|
||||
|
||||
interface SimpleJsonTranslationFile {
|
||||
locale: string;
|
||||
translations: Record<ɵMessageId, ɵSourceMessage>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a semi-public bespoke serialization format that is used for testing and sometimes as a
|
||||
* format for storing translations that will be inlined at runtime.
|
||||
*
|
||||
* @see SimpleJsonTranslationParser
|
||||
*/
|
||||
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) {
|
||||
fileObj.translations[message.id] = message.text;
|
||||
}
|
||||
return JSON.stringify(fileObj, null, 2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @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 {ɵParsedMessage} from '@angular/localize';
|
||||
|
||||
/**
|
||||
* Implement this interface to provide a class that can render messages into a translation file.
|
||||
*/
|
||||
export interface TranslationSerializer {
|
||||
/**
|
||||
* Serialize the contents of a translation file containing the given `messages`.
|
||||
*
|
||||
* @param messages The messages to render to the file.
|
||||
* @returns The contents of the serialized file.
|
||||
*/
|
||||
serialize(messages: ɵParsedMessage[]): string;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* @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 {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) {}
|
||||
|
||||
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'});
|
||||
xml.startTag('file', {'source-language': this.sourceLocale, 'datatype': 'plaintext'});
|
||||
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 {
|
||||
xml.text(message.messageParts[0]);
|
||||
for (let i = 1; i < message.messageParts.length; i++) {
|
||||
xml.startTag('x', {id: message.placeholderNames[i - 1]}, {selfClosing: true});
|
||||
xml.text(message.messageParts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
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 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 this.useLegacyIds && message.legacyIds !== undefined &&
|
||||
message.legacyIds.find(id => id.length === LEGACY_XLIFF_MESSAGE_LENGTH) ||
|
||||
message.id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* @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} from '@angular/localize';
|
||||
|
||||
import {TranslationSerializer} from './translation_serializer';
|
||||
import {XmlFile} from './xml_file';
|
||||
|
||||
/** This is the maximum number of characters that can appear in a legacy XLIFF 2.0 message id. */
|
||||
const MAX_LEGACY_XLIFF_2_MESSAGE_LENGTH = 20;
|
||||
|
||||
/**
|
||||
* A translation serializer that can write translations in XLIFF 2 format.
|
||||
*
|
||||
* http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
|
||||
*
|
||||
* @see Xliff2TranslationParser
|
||||
*/
|
||||
export class Xliff2TranslationSerializer implements TranslationSerializer {
|
||||
constructor(
|
||||
private sourceLocale: string, private basePath: AbsoluteFsPath,
|
||||
private useLegacyIds: boolean) {}
|
||||
|
||||
serialize(messages: ɵParsedMessage[]): string {
|
||||
const ids = new Set<string>();
|
||||
const xml = new XmlFile();
|
||||
xml.startTag('xliff', {
|
||||
'version': '2.0',
|
||||
'xmlns': 'urn:oasis:names:tc:xliff:document:2.0',
|
||||
'srcLang': this.sourceLocale
|
||||
});
|
||||
xml.startTag('file');
|
||||
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('unit', {id});
|
||||
if (message.meaning || message.description) {
|
||||
xml.startTag('notes');
|
||||
if (message.location) {
|
||||
const {file, start, end} = message.location;
|
||||
const endLineString =
|
||||
end !== undefined && end.line !== start.line ? `,${end.line + 1}` : '';
|
||||
this.serializeNote(
|
||||
xml, 'location',
|
||||
`${relative(this.basePath, file)}:${start.line + 1}${endLineString}`);
|
||||
}
|
||||
if (message.description) {
|
||||
this.serializeNote(xml, 'description', message.description);
|
||||
}
|
||||
if (message.meaning) {
|
||||
this.serializeNote(xml, 'meaning', message.meaning);
|
||||
}
|
||||
xml.endTag('notes');
|
||||
}
|
||||
xml.startTag('segment');
|
||||
xml.startTag('source', {}, {preserveWhitespace: true});
|
||||
this.serializeMessage(xml, message);
|
||||
xml.endTag('source', {preserveWhitespace: false});
|
||||
xml.endTag('segment');
|
||||
xml.endTag('unit');
|
||||
}
|
||||
xml.endTag('file');
|
||||
xml.endTag('xliff');
|
||||
return xml.toString();
|
||||
}
|
||||
|
||||
private serializeMessage(xml: XmlFile, message: ɵParsedMessage): void {
|
||||
xml.text(message.messageParts[0]);
|
||||
for (let i = 1; i < message.messageParts.length; i++) {
|
||||
const placeholderName = message.placeholderNames[i - 1];
|
||||
if (placeholderName.startsWith('START_')) {
|
||||
xml.startTag('pc', {
|
||||
id: `${i}`,
|
||||
equivStart: placeholderName,
|
||||
equivEnd: placeholderName.replace(/^START/, 'CLOSE')
|
||||
});
|
||||
} else if (placeholderName.startsWith('CLOSE_')) {
|
||||
xml.endTag('pc');
|
||||
} else {
|
||||
xml.startTag('ph', {id: `${i}`, equiv: placeholderName}, {selfClosing: true});
|
||||
}
|
||||
xml.text(message.messageParts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private serializeNote(xml: XmlFile, name: string, value: string) {
|
||||
xml.startTag('note', {category: name}, {preserveWhitespace: true});
|
||||
xml.text(value);
|
||||
xml.endTag('note', {preserveWhitespace: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id for the given `message`.
|
||||
*
|
||||
* 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 2.0 legacy message id is a 64 bit number encoded as a decimal string, which will have
|
||||
* at most 20 digits, since 2^65-1 = 36,893,488,147,419,103,231. This digest is based on:
|
||||
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/GoogleJsMessageIdGenerator.java
|
||||
*/
|
||||
private getMessageId(message: ɵParsedMessage): string {
|
||||
return this.useLegacyIds && message.legacyIds !== undefined &&
|
||||
message.legacyIds.find(
|
||||
id => id.length <= MAX_LEGACY_XLIFF_2_MESSAGE_LENGTH && !/[^0-9]/.test(id)) ||
|
||||
message.id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* @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 {TranslationSerializer} from './translation_serializer';
|
||||
import {XmlFile} from './xml_file';
|
||||
|
||||
/**
|
||||
* A translation serializer that can write files in XMB format.
|
||||
*
|
||||
* http://cldr.unicode.org/development/development-process/design-proposals/xmb
|
||||
*
|
||||
* @see XmbTranslationParser
|
||||
*/
|
||||
export class XmbTranslationSerializer implements TranslationSerializer {
|
||||
constructor(private basePath: AbsoluteFsPath, private useLegacyIds: boolean) {}
|
||||
|
||||
serialize(messages: ɵParsedMessage[]): string {
|
||||
const ids = new Set<string>();
|
||||
const xml = new XmlFile();
|
||||
xml.rawText(
|
||||
`<!DOCTYPE messagebundle [\n` +
|
||||
`<!ELEMENT messagebundle (msg)*>\n` +
|
||||
`<!ATTLIST messagebundle class CDATA #IMPLIED>\n` +
|
||||
`\n` +
|
||||
`<!ELEMENT msg (#PCDATA|ph|source)*>\n` +
|
||||
`<!ATTLIST msg id CDATA #IMPLIED>\n` +
|
||||
`<!ATTLIST msg seq CDATA #IMPLIED>\n` +
|
||||
`<!ATTLIST msg name CDATA #IMPLIED>\n` +
|
||||
`<!ATTLIST msg desc CDATA #IMPLIED>\n` +
|
||||
`<!ATTLIST msg meaning CDATA #IMPLIED>\n` +
|
||||
`<!ATTLIST msg obsolete (obsolete) #IMPLIED>\n` +
|
||||
`<!ATTLIST msg xml:space (default|preserve) "default">\n` +
|
||||
`<!ATTLIST msg is_hidden CDATA #IMPLIED>\n` +
|
||||
`\n` +
|
||||
`<!ELEMENT source (#PCDATA)>\n` +
|
||||
`\n` +
|
||||
`<!ELEMENT ph (#PCDATA|ex)*>\n` +
|
||||
`<!ATTLIST ph name CDATA #REQUIRED>\n` +
|
||||
`\n` +
|
||||
`<!ELEMENT ex (#PCDATA)>\n` +
|
||||
`]>\n`);
|
||||
xml.startTag('messagebundle');
|
||||
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(
|
||||
'msg', {id, desc: message.description, meaning: message.meaning},
|
||||
{preserveWhitespace: true});
|
||||
if (message.location) {
|
||||
this.serializeLocation(xml, message.location);
|
||||
}
|
||||
this.serializeMessage(xml, message);
|
||||
xml.endTag('msg', {preserveWhitespace: false});
|
||||
}
|
||||
xml.endTag('messagebundle');
|
||||
return xml.toString();
|
||||
}
|
||||
|
||||
private serializeLocation(xml: XmlFile, location: ɵSourceLocation): void {
|
||||
xml.startTag('source');
|
||||
const endLineString = location.end !== undefined && location.end.line !== location.start.line ?
|
||||
`,${location.end.line + 1}` :
|
||||
'';
|
||||
xml.text(`${relative(this.basePath, location.file)}:${location.start.line}${endLineString}`);
|
||||
xml.endTag('source');
|
||||
}
|
||||
|
||||
private serializeMessage(xml: XmlFile, message: ɵParsedMessage): void {
|
||||
xml.text(message.messageParts[0]);
|
||||
for (let i = 1; i < message.messageParts.length; i++) {
|
||||
xml.startTag('ph', {name: message.placeholderNames[i - 1]}, {selfClosing: true});
|
||||
xml.text(message.messageParts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id for the given `message`.
|
||||
*
|
||||
* 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 XMB legacy message id is a 64 bit number encoded as a decimal string, which will have
|
||||
* at most 20 digits, since 2^65-1 = 36,893,488,147,419,103,231. This digest is based on:
|
||||
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/GoogleJsMessageIdGenerator.java
|
||||
*/
|
||||
private getMessageId(message: ɵParsedMessage): string {
|
||||
return this.useLegacyIds && message.legacyIds !== undefined &&
|
||||
message.legacyIds.find(id => id.length <= 20 && !/[^0-9]/.test(id)) ||
|
||||
message.id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
interface Options {
|
||||
selfClosing?: boolean;
|
||||
preserveWhitespace?: boolean;
|
||||
}
|
||||
|
||||
export class XmlFile {
|
||||
private output = '<?xml version="1.0" encoding="UTF-8" ?>\n';
|
||||
private indent = '';
|
||||
private elements: string[] = [];
|
||||
private preservingWhitespace = false;
|
||||
toString() {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
startTag(
|
||||
name: string, attributes: Record<string, string|undefined> = {},
|
||||
{selfClosing = false, preserveWhitespace}: Options = {}): this {
|
||||
if (!this.preservingWhitespace) {
|
||||
this.output += this.indent;
|
||||
}
|
||||
|
||||
this.output += `<${name}`;
|
||||
|
||||
for (const [attrName, attrValue] of Object.entries(attributes)) {
|
||||
if (attrValue) {
|
||||
this.output += ` ${attrName}="${escapeXml(attrValue)}"`;
|
||||
}
|
||||
}
|
||||
|
||||
if (selfClosing) {
|
||||
this.output += '/>';
|
||||
} else {
|
||||
this.output += '>';
|
||||
this.elements.push(name);
|
||||
this.incIndent();
|
||||
}
|
||||
|
||||
if (preserveWhitespace !== undefined) {
|
||||
this.preservingWhitespace = preserveWhitespace;
|
||||
}
|
||||
if (!this.preservingWhitespace) {
|
||||
this.output += `\n`;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
endTag(name: string, {preserveWhitespace}: Options = {}): this {
|
||||
const expectedTag = this.elements.pop();
|
||||
if (expectedTag !== name) {
|
||||
throw new Error(`Unexpected closing tag: "${name}", expected: "${expectedTag}"`);
|
||||
}
|
||||
|
||||
this.decIndent();
|
||||
|
||||
if (!this.preservingWhitespace) {
|
||||
this.output += this.indent;
|
||||
}
|
||||
this.output += `</${name}>`;
|
||||
|
||||
if (preserveWhitespace !== undefined) {
|
||||
this.preservingWhitespace = preserveWhitespace;
|
||||
}
|
||||
if (!this.preservingWhitespace) {
|
||||
this.output += `\n`;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
text(str: string): this {
|
||||
this.output += escapeXml(str);
|
||||
return this;
|
||||
}
|
||||
|
||||
rawText(str: string): this {
|
||||
this.output += str;
|
||||
return this;
|
||||
}
|
||||
|
||||
private incIndent() {
|
||||
this.indent = this.indent + ' ';
|
||||
}
|
||||
private decIndent() {
|
||||
this.indent = this.indent.slice(0, -2);
|
||||
}
|
||||
}
|
||||
|
||||
const _ESCAPED_CHARS: [RegExp, string][] = [
|
||||
[/&/g, '&'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, '''],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
];
|
||||
|
||||
function escapeXml(text: string): string {
|
||||
return _ESCAPED_CHARS.reduce(
|
||||
(text: string, entry: [RegExp, string]) => text.replace(entry[0], entry[1]), text);
|
||||
}
|
|
@ -22,6 +22,8 @@ import {ParsedTranslationBundle, TranslationParser} from './translation_parser';
|
|||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see SimpleJsonTranslationSerializer
|
||||
*/
|
||||
export class SimpleJsonTranslationParser implements TranslationParser<Object> {
|
||||
canParse(filePath: string, contents: string): Object|false {
|
||||
|
|
|
@ -22,6 +22,7 @@ import {addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedEle
|
|||
* 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 Xliff1TranslationSerializer
|
||||
*/
|
||||
export class Xliff1TranslationParser implements TranslationParser<XmlTranslationParserHint> {
|
||||
canParse(filePath: string, contents: string): XmlTranslationParserHint|false {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedEle
|
|||
*
|
||||
* http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
|
||||
*
|
||||
* @see Xliff2TranslationSerializer
|
||||
*/
|
||||
export class Xliff2TranslationParser implements TranslationParser<XmlTranslationParserHint> {
|
||||
canParse(filePath: string, contents: string): XmlTranslationParserHint|false {
|
||||
|
|
|
@ -19,7 +19,11 @@ import {addParseDiagnostic, addParseError, canParseXml, getAttribute, parseInner
|
|||
|
||||
|
||||
/**
|
||||
* A translation parser that can load XB files.
|
||||
* A translation parser that can load XTB files.
|
||||
*
|
||||
* http://cldr.unicode.org/development/development-process/design-proposals/xmb
|
||||
*
|
||||
* @see XmbTranslationSerializer
|
||||
*/
|
||||
export class XtbTranslationParser implements TranslationParser<XmlTranslationParserHint> {
|
||||
canParse(filePath: string, contents: string): XmlTranslationParserHint|false {
|
||||
|
|
|
@ -4,15 +4,17 @@ ts_library(
|
|||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["**/*_spec.ts"],
|
||||
["**/*.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/logging/testing",
|
||||
"//packages/localize",
|
||||
"//packages/localize/src/tools",
|
||||
"//packages/localize/src/utils",
|
||||
"@npm//@babel/core",
|
||||
"@npm//@babel/generator",
|
||||
"@npm//@babel/template",
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* @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 {absoluteFrom, getFileSystem, relativeFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||
import {MockLogger} from '@angular/compiler-cli/src/ngtsc/logging/testing';
|
||||
|
||||
import {MessageExtractor} from '../../src/extract/extraction';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('extractMessages', () => {
|
||||
it('should extract a message for each $localize template tag', () => {
|
||||
const fs = getFileSystem();
|
||||
const logger = new MockLogger();
|
||||
const basePath = absoluteFrom('/root/path/');
|
||||
const filename = 'relative/path.js';
|
||||
const file = fs.resolve(basePath, filename);
|
||||
const extractor = new MessageExtractor(fs, logger, {basePath});
|
||||
fs.ensureDir(absoluteFrom('/root/path/relative'));
|
||||
fs.writeFile(file, [
|
||||
'$localize`:meaning|description:a${1}b${2}c`;',
|
||||
'$localize(__makeTemplateObject(["a", ":custom-placeholder:b", "c"], ["a", ":custom-placeholder:b", "c"]), 1, 2);'
|
||||
].join('\n'));
|
||||
const messages = extractor.extractMessages(filename);
|
||||
|
||||
expect(messages.length).toEqual(2);
|
||||
|
||||
expect(messages[0]).toEqual({
|
||||
id: '2714330828844000684',
|
||||
description: 'description',
|
||||
meaning: 'meaning',
|
||||
messageParts: ['a', 'b', 'c'],
|
||||
text: 'a{$PH}b{$PH_1}c',
|
||||
placeholderNames: ['PH', 'PH_1'],
|
||||
substitutions: jasmine.any(Object),
|
||||
legacyIds: [],
|
||||
location: {start: {line: 0, column: 9}, end: {line: 0, column: 43}, file},
|
||||
});
|
||||
|
||||
expect(messages[1]).toEqual({
|
||||
id: '5692770902395945649',
|
||||
description: '',
|
||||
meaning: '',
|
||||
messageParts: ['a', 'b', 'c'],
|
||||
text: 'a{$custom-placeholder}b{$PH_1}c',
|
||||
placeholderNames: ['custom-placeholder', 'PH_1'],
|
||||
substitutions: jasmine.any(Object),
|
||||
legacyIds: [],
|
||||
location: {start: {line: 1, column: 0}, end: {line: 1, column: 111}, file},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["**/*_spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/logging",
|
||||
"//packages/compiler-cli/src/ngtsc/logging/testing",
|
||||
"//packages/compiler-cli/test/helpers",
|
||||
"//packages/localize/src/tools",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "integration",
|
||||
bootstrap = ["//tools/testing:node_no_angular_es5"],
|
||||
data = [
|
||||
"//packages/localize/src/tools/test/extract/integration/test_files",
|
||||
"//packages/localize/src/tools/test/extract/integration/test_files:compile_es2015",
|
||||
"//packages/localize/src/tools/test/extract/integration/test_files:compile_es5",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
"@npm//glob",
|
||||
"@npm//yargs",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,228 @@
|
|||
/**
|
||||
* @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 {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||
import {Logger} from '@angular/compiler-cli/src/ngtsc/logging';
|
||||
import {MockLogger} from '@angular/compiler-cli/src/ngtsc/logging/testing';
|
||||
import {loadTestDirectory} from '@angular/compiler-cli/test/helpers';
|
||||
|
||||
import {extractTranslations} from '../../../src/extract/main';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
let fs: FileSystem;
|
||||
let logger: Logger;
|
||||
let rootPath: AbsoluteFsPath;
|
||||
let outputPath: AbsoluteFsPath;
|
||||
let sourceFilePath: AbsoluteFsPath;
|
||||
let textFile1: AbsoluteFsPath;
|
||||
let textFile2: AbsoluteFsPath;
|
||||
|
||||
beforeEach(() => {
|
||||
fs = getFileSystem();
|
||||
logger = new MockLogger();
|
||||
rootPath = absoluteFrom('/project');
|
||||
outputPath = fs.resolve(rootPath, 'extracted-message-file');
|
||||
sourceFilePath = fs.resolve(rootPath, 'test_files/test.js');
|
||||
textFile1 = fs.resolve(rootPath, 'test_files/test-1.txt');
|
||||
textFile2 = fs.resolve(rootPath, 'test_files/test-2.txt');
|
||||
|
||||
fs.ensureDir(fs.dirname(sourceFilePath));
|
||||
loadTestDirectory(fs, __dirname + '/test_files', absoluteFrom('/project/test_files'));
|
||||
});
|
||||
|
||||
describe('extractTranslations()', () => {
|
||||
it('should ignore non-code files', () => {
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceLocale: 'en',
|
||||
sourceFilePaths: [],
|
||||
format: 'json',
|
||||
outputPath,
|
||||
logger,
|
||||
useSourceMaps: false,
|
||||
useLegacyIds: false,
|
||||
});
|
||||
expect(fs.readFile(outputPath)).toEqual([
|
||||
`{`,
|
||||
` "locale": "en",`,
|
||||
` "translations": {}`,
|
||||
`}`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should extract translations from source code, and write as JSON format', () => {
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceLocale: 'en-GB',
|
||||
sourceFilePaths: [sourceFilePath],
|
||||
format: 'json',
|
||||
outputPath,
|
||||
logger,
|
||||
useSourceMaps: false,
|
||||
useLegacyIds: false,
|
||||
});
|
||||
expect(fs.readFile(outputPath)).toEqual([
|
||||
`{`,
|
||||
` "locale": "en-GB",`,
|
||||
` "translations": {`,
|
||||
` "3291030485717846467": "Hello, {$PH}!",`,
|
||||
` "8669027859022295761": "try{$PH}me"`,
|
||||
` }`,
|
||||
`}`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should extract translations from source code, and write as xmb format', () => {
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceLocale: 'en',
|
||||
sourceFilePaths: [sourceFilePath],
|
||||
format: 'xmb',
|
||||
outputPath,
|
||||
logger,
|
||||
useSourceMaps: false,
|
||||
useLegacyIds: false,
|
||||
});
|
||||
expect(fs.readFile(outputPath)).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="3291030485717846467"><source>test_files/test.js:1</source>Hello, <ph name="PH"/>!</msg>`,
|
||||
` <msg id="8669027859022295761"><source>test_files/test.js:2</source>try<ph name="PH"/>me</msg>`,
|
||||
`</messagebundle>\n`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should extract translations from source code, and write as XLIFF 1.2 format', () => {
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceLocale: 'en-CA',
|
||||
sourceFilePaths: [sourceFilePath],
|
||||
format: 'xliff',
|
||||
outputPath,
|
||||
logger,
|
||||
useSourceMaps: false,
|
||||
useLegacyIds: false,
|
||||
});
|
||||
expect(fs.readFile(outputPath)).toEqual([
|
||||
`<?xml version="1.0" encoding="UTF-8" ?>`,
|
||||
`<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">`,
|
||||
` <file source-language="en-CA" datatype="plaintext">`,
|
||||
` <body>`,
|
||||
` <trans-unit id="3291030485717846467" datatype="html">`,
|
||||
` <source>Hello, <x id="PH"/>!</source>`,
|
||||
` <context-group purpose="location">`,
|
||||
` <context context-type="sourcefile">test_files/test.js</context>`,
|
||||
` <context context-type="linenumber">2</context>`,
|
||||
` </context-group>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="8669027859022295761" datatype="html">`,
|
||||
` <source>try<x id="PH"/>me</source>`,
|
||||
` <context-group purpose="location">`,
|
||||
` <context context-type="sourcefile">test_files/test.js</context>`,
|
||||
` <context context-type="linenumber">3</context>`,
|
||||
` </context-group>`,
|
||||
` </trans-unit>`,
|
||||
` </body>`,
|
||||
` </file>`,
|
||||
`</xliff>\n`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should extract translations from source code, and write as XLIFF 2 format', () => {
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceLocale: 'en-AU',
|
||||
sourceFilePaths: [sourceFilePath],
|
||||
format: 'xliff2',
|
||||
outputPath,
|
||||
logger,
|
||||
useSourceMaps: false,
|
||||
useLegacyIds: false,
|
||||
});
|
||||
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>`,
|
||||
` <unit id="3291030485717846467">`,
|
||||
` <segment>`,
|
||||
` <source>Hello, <ph id="1" equiv="PH"/>!</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="8669027859022295761">`,
|
||||
` <segment>`,
|
||||
` <source>try<ph id="1" equiv="PH"/>me</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` </file>`,
|
||||
`</xliff>\n`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
for (const target of ['es2015', 'es5']) {
|
||||
it(`should render the original location of translations, when processing an ${
|
||||
target} bundle with source-maps`,
|
||||
() => {
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceLocale: 'en-CA',
|
||||
sourceFilePaths: [fs.resolve(rootPath, `test_files/dist_${target}/index.js`)],
|
||||
format: 'xliff',
|
||||
outputPath,
|
||||
logger,
|
||||
useSourceMaps: true,
|
||||
useLegacyIds: false,
|
||||
});
|
||||
expect(fs.readFile(outputPath)).toEqual([
|
||||
`<?xml version="1.0" encoding="UTF-8" ?>`,
|
||||
`<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">`,
|
||||
` <file source-language="en-CA" datatype="plaintext">`,
|
||||
` <body>`,
|
||||
` <trans-unit id="157258427077572998" datatype="html">`,
|
||||
` <source>Message in <x id="a-file"/>!</source>`,
|
||||
` <context-group purpose="location">`,
|
||||
// These source file paths are due to how Bazel TypeScript compilation source-maps work
|
||||
` <context context-type="sourcefile">../packages/localize/src/tools/test/extract/integration/test_files/src/a.ts</context>`,
|
||||
` <context context-type="linenumber">3</context>`,
|
||||
` </context-group>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="7829869508202074508" datatype="html">`,
|
||||
` <source>Message in <x id="b-file"/>!</source>`,
|
||||
` <context-group purpose="location">`,
|
||||
` <context context-type="sourcefile">../packages/localize/src/tools/test/extract/integration/test_files/src/b.ts</context>`,
|
||||
` <context context-type="linenumber">3</context>`,
|
||||
` </context-group>`,
|
||||
` </trans-unit>`,
|
||||
` </body>`,
|
||||
` </file>`,
|
||||
`</xliff>\n`,
|
||||
].join('\n'));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
package(default_visibility = ["//packages/localize/src/tools/test/extract/integration:__pkg__"])
|
||||
|
||||
load("@npm//typescript:index.bzl", "tsc")
|
||||
|
||||
tsc(
|
||||
name = "compile_es5",
|
||||
outs = [
|
||||
"dist_es5/index.js",
|
||||
"dist_es5/index.js.map",
|
||||
],
|
||||
args = [
|
||||
"--target",
|
||||
"es5",
|
||||
"--module",
|
||||
"amd",
|
||||
"--outFile",
|
||||
"$(execpath dist_es5/index.js)",
|
||||
"--skipLibCheck",
|
||||
"--sourceMap",
|
||||
"--inlineSources",
|
||||
"$(execpath src/index.ts)",
|
||||
],
|
||||
data = glob(["src/*.ts"]),
|
||||
)
|
||||
|
||||
tsc(
|
||||
name = "compile_es2015",
|
||||
outs = [
|
||||
"dist_es2015/index.js",
|
||||
"dist_es2015/index.js.map",
|
||||
],
|
||||
args = [
|
||||
"--target",
|
||||
"es2015",
|
||||
"--module",
|
||||
"amd",
|
||||
"--outFile",
|
||||
"$(execpath dist_es2015/index.js)",
|
||||
"--skipLibCheck",
|
||||
"--sourceMap",
|
||||
"--inlineSources",
|
||||
"$(execpath src/index.ts)",
|
||||
],
|
||||
data = glob(["src/*.ts"]),
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "test_files",
|
||||
srcs = glob([
|
||||
"**/*.js",
|
||||
"**/*.txt",
|
||||
"**/*.ts",
|
||||
]),
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
declare const $localize: any;
|
||||
const file = 'a.ts';
|
||||
export const messageA = $localize`Message in ${file}:a-file:!`;
|
|
@ -0,0 +1,3 @@
|
|||
declare const $localize: any;
|
||||
const file = 'b.ts';
|
||||
export const messageB = $localize`Message in ${file}:b-file:!`;
|
|
@ -0,0 +1,2 @@
|
|||
export * from './a';
|
||||
export * from './b';
|
|
@ -0,0 +1 @@
|
|||
Contents of test-1.txt
|
|
@ -0,0 +1 @@
|
|||
Contents of test-2.txt
|
|
@ -0,0 +1,3 @@
|
|||
var name = 'World';
|
||||
var message = $localize`Hello, ${name}!`;
|
||||
var other = $localize(__makeTemplateObject(['try', 'me'], ['try', 'me']), 40 + 2);
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* @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 {ɵParsedMessage} from '@angular/localize';
|
||||
|
||||
import {SimpleJsonTranslationSerializer} from '../../../src/extract/translation_files/json_translation_serializer';
|
||||
|
||||
import {mockMessage} from './mock_message';
|
||||
|
||||
describe('JsonTranslationSerializer', () => {
|
||||
describe('renderFile()', () => {
|
||||
it('should convert a set of parsed messages into a JSON string', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], {meaning: 'some meaning'}),
|
||||
mockMessage(
|
||||
'67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'],
|
||||
{description: 'some description'}),
|
||||
mockMessage('13579', ['', 'b', ''], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {}),
|
||||
mockMessage('24680', ['a'], [], {meaning: 'meaning', description: 'and description'}),
|
||||
mockMessage('80808', ['multi\nlines'], [], {}),
|
||||
];
|
||||
const serializer = new SimpleJsonTranslationSerializer('xx');
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output).toEqual([
|
||||
`{`,
|
||||
` "locale": "xx",`,
|
||||
` "translations": {`,
|
||||
` "12345": "a{$PH}b{$PH_1}c",`,
|
||||
` "13579": "{$START_BOLD_TEXT}b{$CLOSE_BOLD_TEXT}",`,
|
||||
` "24680": "a",`,
|
||||
` "67890": "a{$START_TAG_SPAN}{$CLOSE_TAG_SPAN}c",`,
|
||||
` "80808": "multi\\nlines"`,
|
||||
` }`,
|
||||
`}`,
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @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 {ɵParsedMessage} from '@angular/localize';
|
||||
import {SourceLocation} from '@angular/localize/src/utils';
|
||||
|
||||
export interface MockMessageOptions {
|
||||
meaning?: string;
|
||||
description?: string;
|
||||
location?: SourceLocation;
|
||||
legacyIds?: string[];
|
||||
}
|
||||
/**
|
||||
* This helper is used to create `ParsedMessage` objects to be rendered in the
|
||||
* `TranslationSerializer` tests.
|
||||
*/
|
||||
export function mockMessage(
|
||||
id: string, messageParts: string[], placeholderNames: string[],
|
||||
{meaning = '', description = '', location, legacyIds = []}: MockMessageOptions):
|
||||
ɵParsedMessage {
|
||||
let text = messageParts[0];
|
||||
for (let i = 1; i < messageParts.length; i++) {
|
||||
text += `{$${placeholderNames[i - 1]}}${messageParts[i]}`;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
text,
|
||||
messageParts,
|
||||
placeholderNames,
|
||||
description,
|
||||
meaning,
|
||||
substitutions: [],
|
||||
legacyIds,
|
||||
location,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* @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 {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||
import {ɵParsedMessage} from '@angular/localize';
|
||||
|
||||
import {Xliff1TranslationSerializer} from '../../../src/extract/translation_files/xliff1_translation_serializer';
|
||||
|
||||
import {mockMessage} from './mock_message';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('Xliff1TranslationSerializer', () => {
|
||||
[false, true].forEach(useLegacyIds => {
|
||||
describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => {
|
||||
it('should convert a set of parsed messages into an XML string', () => {
|
||||
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}
|
||||
},
|
||||
legacyIds: ['1234567890ABCDEF1234567890ABCDEF12345678', '615790887472569365'],
|
||||
}),
|
||||
mockMessage(
|
||||
'67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'],
|
||||
{description: 'some description'}),
|
||||
mockMessage('13579', ['', 'b', ''], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {}),
|
||||
mockMessage('24680', ['a'], [], {meaning: 'meaning', description: 'and description'}),
|
||||
mockMessage('80808', ['multi\nlines'], [], {}),
|
||||
mockMessage('90000', ['<escape', 'me>'], ['double-quotes-"'], {})
|
||||
];
|
||||
const serializer =
|
||||
new Xliff1TranslationSerializer('xx', absoluteFrom('/project'), useLegacyIds);
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output).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">`,
|
||||
` <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="67890" datatype="html">`,
|
||||
` <source>a<x id="START_TAG_SPAN"/><x id="CLOSE_TAG_SPAN"/>c</source>`,
|
||||
` <note priority="1" from="description">some description</note>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="13579" datatype="html">`,
|
||||
` <source><x id="START_BOLD_TEXT"/>b<x id="CLOSE_BOLD_TEXT"/></source>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="24680" datatype="html">`,
|
||||
` <source>a</source>`,
|
||||
` <note priority="1" from="description">and description</note>`,
|
||||
` <note priority="1" from="meaning">meaning</note>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="80808" datatype="html">`,
|
||||
` <source>multi`,
|
||||
`lines</source>`,
|
||||
` </trans-unit>`,
|
||||
` <trans-unit id="90000" datatype="html">`,
|
||||
` <source><escape<x id="double-quotes-""/>me></source>`,
|
||||
` </trans-unit>`,
|
||||
` </body>`,
|
||||
` </file>`,
|
||||
`</xliff>\n`,
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* @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 {computeMsgId} from '@angular/compiler';
|
||||
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||
import {ɵParsedMessage} from '@angular/localize';
|
||||
|
||||
import {Xliff2TranslationSerializer} from '../../../src/extract/translation_files/xliff2_translation_serializer';
|
||||
|
||||
import {mockMessage} from './mock_message';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('Xliff2TranslationSerializer', () => {
|
||||
[false, true].forEach(useLegacyIds => {
|
||||
describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => {
|
||||
it('should convert a set of parsed messages into an XML string', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], {
|
||||
meaning: 'some meaning',
|
||||
location: {
|
||||
file: absoluteFrom('/project/file.ts'),
|
||||
start: {line: 5, column: 0},
|
||||
end: {line: 5, column: 3}
|
||||
},
|
||||
legacyIds: ['1234567890ABCDEF1234567890ABCDEF12345678', '615790887472569365'],
|
||||
}),
|
||||
mockMessage('67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {
|
||||
description: 'some description',
|
||||
location: {
|
||||
file: absoluteFrom('/project/file.ts'),
|
||||
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('80808', ['multi\nlines'], [], {}),
|
||||
mockMessage('90000', ['<escape', 'me>'], ['double-quotes-"'], {})
|
||||
];
|
||||
const serializer =
|
||||
new Xliff2TranslationSerializer('xx', absoluteFrom('/project'), useLegacyIds);
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output).toEqual([
|
||||
`<?xml version="1.0" encoding="UTF-8" ?>`,
|
||||
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="xx">`,
|
||||
` <file>`,
|
||||
` <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="1" equiv="PH"/>b<ph id="2" 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="1" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN"></pc>c</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="13579">`,
|
||||
` <segment>`,
|
||||
` <source><pc id="1" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT">b</pc></source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="24680">`,
|
||||
` <notes>`,
|
||||
` <note category="description">and description</note>`,
|
||||
` <note category="meaning">meaning</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>a</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="80808">`,
|
||||
` <segment>`,
|
||||
` <source>multi`,
|
||||
`lines</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="90000">`,
|
||||
` <segment>`,
|
||||
` <source><escape<ph id="1" equiv="double-quotes-""/>me></source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` </file>`,
|
||||
`</xliff>\n`,
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @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 {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||
import {ɵParsedMessage} from '@angular/localize';
|
||||
|
||||
import {XmbTranslationSerializer} from '../../../src/extract/translation_files/xmb_translation_serializer';
|
||||
|
||||
import {mockMessage} from './mock_message';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('XmbTranslationSerializer', () => {
|
||||
[false, true].forEach(useLegacyIds => {
|
||||
describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => {
|
||||
it('should convert a set of parsed messages into an XML string', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], {
|
||||
meaning: 'some meaning',
|
||||
legacyIds: ['1234567890ABCDEF1234567890ABCDEF12345678', '615790887472569365'],
|
||||
}),
|
||||
mockMessage(
|
||||
'67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'],
|
||||
{description: 'some description'}),
|
||||
mockMessage('13579', ['', 'b', ''], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {}),
|
||||
mockMessage('24680', ['a'], [], {meaning: 'meaning', description: 'and description'}),
|
||||
mockMessage('80808', ['multi\nlines'], [], {}),
|
||||
mockMessage('90000', ['<escape', 'me>'], ['double-quotes-"'], {}),
|
||||
];
|
||||
const serializer = new XmbTranslationSerializer(absoluteFrom('/project'), useLegacyIds);
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output).toContain([
|
||||
`<messagebundle>`,
|
||||
` <msg id="${
|
||||
useLegacyIds ?
|
||||
'615790887472569365' :
|
||||
'12345'}" meaning="some meaning">a<ph name="PH"/>b<ph name="PH_1"/>c</msg>`,
|
||||
` <msg id="67890" desc="some description">a<ph name="START_TAG_SPAN"/><ph name="CLOSE_TAG_SPAN"/>c</msg>`,
|
||||
` <msg id="13579"><ph name="START_BOLD_TEXT"/>b<ph name="CLOSE_BOLD_TEXT"/></msg>`,
|
||||
` <msg id="24680" desc="and description" meaning="meaning">a</msg>`,
|
||||
` <msg id="80808">multi`, `lines</msg>`,
|
||||
` <msg id="90000"><escape<ph name="double-quotes-""/>me></msg>`,
|
||||
`</messagebundle>\n`
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -152,7 +152,7 @@ export function parseMessage(
|
|||
cleanedMessageParts.push(messagePart);
|
||||
}
|
||||
const messageId = metadata.id || computeMsgId(messageString, metadata.meaning || '');
|
||||
const legacyIds = metadata.legacyIds && metadata.legacyIds.filter(id => id !== messageId);
|
||||
const legacyIds = metadata.legacyIds ? metadata.legacyIds.filter(id => id !== messageId) : [];
|
||||
return {
|
||||
id: messageId,
|
||||
legacyIds,
|
||||
|
|
Loading…
Reference in New Issue