feat(localize): add scripts to migrate away from legacy message IDs (#41026)
Adds a new flag to `localize-extract` called `--migrateMapFile` which will generate a JSON file that can be used to map legacy message IDs to cannonical ones. Also includes a new script called `localize-migrate` that can take the mapping file which was generated by `localize-extract` and migrate all of the IDs in the files that were passed in. PR Close #41026
This commit is contained in:
parent
531f0bfb4a
commit
1735430476
|
@ -4,7 +4,8 @@
|
|||
"description": "Angular - library for localizing messages",
|
||||
"bin": {
|
||||
"localize-translate": "./src/tools/src/translate/main.js",
|
||||
"localize-extract": "./src/tools/src/extract/main.js"
|
||||
"localize-extract": "./src/tools/src/extract/main.js",
|
||||
"localize-migrate": "./src/tools/src/migrate/main.js"
|
||||
},
|
||||
"author": "angular",
|
||||
"license": "MIT",
|
||||
|
|
|
@ -12,7 +12,7 @@ import {ɵParsedMessage} from '@angular/localize';
|
|||
import * as glob from 'glob';
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {DiagnosticHandlingStrategy} from '../diagnostics';
|
||||
import {Diagnostics, DiagnosticHandlingStrategy} from '../diagnostics';
|
||||
|
||||
import {checkDuplicateMessages} from './duplicates';
|
||||
import {MessageExtractor} from './extraction';
|
||||
|
@ -23,6 +23,7 @@ import {Xliff1TranslationSerializer} from './translation_files/xliff1_translatio
|
|||
import {Xliff2TranslationSerializer} from './translation_files/xliff2_translation_serializer';
|
||||
import {XmbTranslationSerializer} from './translation_files/xmb_translation_serializer';
|
||||
import {FormatOptions, parseFormatOptions} from './translation_files/format_options';
|
||||
import {LegacyMessageIdMigrationSerializer} from './translation_files/legacy_message_id_migration_serializer';
|
||||
|
||||
if (require.main === module) {
|
||||
process.title = 'Angular Localization Message Extractor (localize-extract)';
|
||||
|
@ -53,7 +54,9 @@ if (require.main === module) {
|
|||
.option('f', {
|
||||
alias: 'format',
|
||||
required: true,
|
||||
choices: ['xmb', 'xlf', 'xlif', 'xliff', 'xlf2', 'xlif2', 'xliff2', 'json'],
|
||||
choices: [
|
||||
'xmb', 'xlf', 'xlif', 'xliff', 'xlf2', 'xlif2', 'xliff2', 'json', 'legacy-migrate'
|
||||
],
|
||||
describe: 'The format of the translation file.',
|
||||
type: 'string',
|
||||
})
|
||||
|
@ -108,17 +111,17 @@ if (require.main === module) {
|
|||
const logger = new ConsoleLogger(logLevel ? LogLevel[logLevel] : LogLevel.warn);
|
||||
const duplicateMessageHandling = options.d as DiagnosticHandlingStrategy;
|
||||
const formatOptions = parseFormatOptions(options.formatOptions);
|
||||
|
||||
const format = options.f;
|
||||
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceFilePaths,
|
||||
sourceLocale: options.l,
|
||||
format: options.f,
|
||||
format,
|
||||
outputPath: options.o,
|
||||
logger,
|
||||
useSourceMaps: options.useSourceMaps,
|
||||
useLegacyIds: options.useLegacyIds,
|
||||
useLegacyIds: format === 'legacy-migrate' || options.useLegacyIds,
|
||||
duplicateMessageHandling,
|
||||
formatOptions,
|
||||
fileSystem,
|
||||
|
@ -202,8 +205,8 @@ export function extractTranslations({
|
|||
}
|
||||
|
||||
const outputPath = fs.resolve(rootPath, output);
|
||||
const serializer =
|
||||
getSerializer(format, sourceLocale, fs.dirname(outputPath), useLegacyIds, formatOptions, fs);
|
||||
const serializer = getSerializer(
|
||||
format, sourceLocale, fs.dirname(outputPath), useLegacyIds, formatOptions, fs, diagnostics);
|
||||
const translationFile = serializer.serialize(messages);
|
||||
fs.ensureDir(fs.dirname(outputPath));
|
||||
fs.writeFile(outputPath, translationFile);
|
||||
|
@ -213,9 +216,10 @@ export function extractTranslations({
|
|||
}
|
||||
}
|
||||
|
||||
export function getSerializer(
|
||||
function getSerializer(
|
||||
format: string, sourceLocale: string, rootPath: AbsoluteFsPath, useLegacyIds: boolean,
|
||||
formatOptions: FormatOptions = {}, fs: PathManipulation): TranslationSerializer {
|
||||
formatOptions: FormatOptions = {}, fs: PathManipulation,
|
||||
diagnostics: Diagnostics): TranslationSerializer {
|
||||
switch (format) {
|
||||
case 'xlf':
|
||||
case 'xlif':
|
||||
|
@ -233,6 +237,8 @@ export function getSerializer(
|
|||
return new SimpleJsonTranslationSerializer(sourceLocale);
|
||||
case 'arb':
|
||||
return new ArbTranslationSerializer(sourceLocale, rootPath, fs);
|
||||
case 'legacy-migrate':
|
||||
return new LegacyMessageIdMigrationSerializer(diagnostics);
|
||||
}
|
||||
throw new Error(`No translation serializer can handle the provided format: ${format}`);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @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 as ParsedMessage} from '@angular/localize';
|
||||
import {Diagnostics} from '../../diagnostics';
|
||||
import {TranslationSerializer} from './translation_serializer';
|
||||
|
||||
|
||||
/**
|
||||
* A translation serializer that generates the mapping file for the legacy message ID migration.
|
||||
* The file is used by the `localize-migrate` script to migrate existing translation files from
|
||||
* the legacy message IDs to the canonical ones.
|
||||
*/
|
||||
export class LegacyMessageIdMigrationSerializer implements TranslationSerializer {
|
||||
constructor(private _diagnostics: Diagnostics) {}
|
||||
|
||||
serialize(messages: ParsedMessage[]): string {
|
||||
let hasMessages = false;
|
||||
const mapping = messages.reduce((output, message) => {
|
||||
if (shouldMigrate(message)) {
|
||||
for (const legacyId of message.legacyIds!) {
|
||||
if (output.hasOwnProperty(legacyId)) {
|
||||
this._diagnostics.warn(`Detected duplicate legacy ID ${legacyId}.`);
|
||||
}
|
||||
|
||||
output[legacyId] = message.id;
|
||||
hasMessages = true;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
if (!hasMessages) {
|
||||
this._diagnostics.warn(
|
||||
'Could not find any legacy message IDs in source files while generating ' +
|
||||
'the legacy message migration file.');
|
||||
}
|
||||
|
||||
return JSON.stringify(mapping, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if a message needs to be migrated. */
|
||||
function shouldMigrate(message: ParsedMessage): boolean {
|
||||
return !message.customId && !!message.legacyIds && message.legacyIds.length > 0;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
#!/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, NodeJSFileSystem, setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {ConsoleLogger, Logger, LogLevel} from '@angular/compiler-cli/src/ngtsc/logging';
|
||||
import * as glob from 'glob';
|
||||
import * as yargs from 'yargs';
|
||||
import {migrateFile, MigrationMapping} from './migrate';
|
||||
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const options =
|
||||
yargs
|
||||
.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.',
|
||||
type: 'string',
|
||||
})
|
||||
.option('f', {
|
||||
alias: 'files',
|
||||
required: true,
|
||||
describe:
|
||||
'A glob pattern indicating what files to migrate. This should be relative to the root path',
|
||||
type: 'string',
|
||||
})
|
||||
.option('m', {
|
||||
alias: 'mapFile',
|
||||
required: true,
|
||||
describe:
|
||||
'Path to the migration mapping file generated by `localize-extract`. This should be relative to the root path.',
|
||||
type: 'string',
|
||||
})
|
||||
.strict()
|
||||
.help()
|
||||
.parse(args);
|
||||
|
||||
const fs = new NodeJSFileSystem();
|
||||
setFileSystem(fs);
|
||||
|
||||
const rootPath = options.r;
|
||||
const translationFilePaths = glob.sync(options.f, {cwd: rootPath, nodir: true});
|
||||
const logger = new ConsoleLogger(LogLevel.warn);
|
||||
|
||||
migrateFiles({rootPath, translationFilePaths, mappingFilePath: options.m, logger});
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
export interface MigrateFilesOptions {
|
||||
/**
|
||||
* The base path for other paths provided in these options.
|
||||
* This should either be absolute or relative to the current working directory.
|
||||
*/
|
||||
rootPath: string;
|
||||
|
||||
/** Paths to the files that should be migrated. Should be relative to the `rootPath`. */
|
||||
translationFilePaths: string[];
|
||||
|
||||
/** Path to the file containing the message ID mappings. Should be relative to the `rootPath`. */
|
||||
mappingFilePath: string;
|
||||
|
||||
/** Logger to use for diagnostic messages. */
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
/** Migrates the legacy message IDs based on the passed in configuration. */
|
||||
export function migrateFiles({
|
||||
rootPath,
|
||||
translationFilePaths,
|
||||
mappingFilePath,
|
||||
logger,
|
||||
}: MigrateFilesOptions) {
|
||||
const fs = getFileSystem();
|
||||
const absoluteMappingPath = fs.resolve(rootPath, mappingFilePath);
|
||||
const mapping = JSON.parse(fs.readFile(absoluteMappingPath)) as MigrationMapping;
|
||||
|
||||
if (Object.keys(mapping).length === 0) {
|
||||
logger.warn(
|
||||
`Mapping file at ${absoluteMappingPath} is empty. Either there are no messages ` +
|
||||
`that need to be migrated, or the extraction step failed to find them.`);
|
||||
} else {
|
||||
translationFilePaths.forEach(path => {
|
||||
const absolutePath = fs.resolve(rootPath, path);
|
||||
const sourceCode = fs.readFile(absolutePath);
|
||||
fs.writeFile(absolutePath, migrateFile(sourceCode, mapping));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
||||
/** Mapping between legacy message IDs and their cannonical counterparts. */
|
||||
export type MigrationMapping = {
|
||||
[legacyId: string]: string;
|
||||
};
|
||||
|
||||
/** Migrates the legacy message IDs within a single file. */
|
||||
export function migrateFile(sourceCode: string, mapping: MigrationMapping) {
|
||||
const legacyIds = Object.keys(mapping);
|
||||
|
||||
for (const legacyId of legacyIds) {
|
||||
const cannonicalId = mapping[legacyId];
|
||||
const pattern = new RegExp(escapeRegExp(legacyId), 'g');
|
||||
sourceCode = sourceCode.replace(pattern, cannonicalId);
|
||||
}
|
||||
|
||||
return sourceCode;
|
||||
}
|
||||
|
||||
/** Escapes special regex characters in a string. */
|
||||
function escapeRegExp(str: string): string {
|
||||
return str.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
|
||||
}
|
|
@ -494,6 +494,49 @@ runInNativeFileSystem(() => {
|
|||
`}`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should generate the migration map file, if requested', () => {
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceLocale: 'en',
|
||||
sourceFilePaths: [sourceFilePath],
|
||||
format: 'legacy-migrate',
|
||||
outputPath,
|
||||
logger,
|
||||
useSourceMaps: false,
|
||||
useLegacyIds: true,
|
||||
duplicateMessageHandling: 'ignore',
|
||||
fileSystem: fs
|
||||
});
|
||||
expect(fs.readFile(outputPath)).toEqual([
|
||||
`{`,
|
||||
` "1234567890123456789012345678901234567890": "273296103957933077",`,
|
||||
` "12345678901234567890": "273296103957933077"`,
|
||||
`}`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should log a warning if there are no legacy message IDs to migrate', () => {
|
||||
extractTranslations({
|
||||
rootPath,
|
||||
sourceLocale: 'en',
|
||||
sourceFilePaths: [textFile1],
|
||||
format: 'legacy-migrate',
|
||||
outputPath,
|
||||
logger,
|
||||
useSourceMaps: false,
|
||||
useLegacyIds: true,
|
||||
duplicateMessageHandling: 'ignore',
|
||||
fileSystem: fs
|
||||
});
|
||||
|
||||
expect(fs.readFile(outputPath)).toBe('{}');
|
||||
expect(logger.logs.warn).toEqual([[
|
||||
'Messages extracted with warnings\n' +
|
||||
'WARNINGS:\n' +
|
||||
' - Could not find any legacy message IDs in source files while generating the legacy message migration file.'
|
||||
]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* @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 {Diagnostics} from '../../../src/diagnostics';
|
||||
import {LegacyMessageIdMigrationSerializer} from '../../../src/extract/translation_files/legacy_message_id_migration_serializer';
|
||||
import {mockMessage} from './mock_message';
|
||||
|
||||
// Doesn't need to run in each file system since it doesn't interact with the file system.
|
||||
|
||||
describe('LegacyMessageIdMigrationSerializer', () => {
|
||||
let serializer: LegacyMessageIdMigrationSerializer;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = new LegacyMessageIdMigrationSerializer(new Diagnostics());
|
||||
});
|
||||
|
||||
it('should convert a set of parsed messages into a migration mapping file', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('one', [], [], {legacyIds: ['legacy-one', 'other-legacy-one']}),
|
||||
mockMessage('two', [], [], {legacyIds: ['legacy-two']}),
|
||||
mockMessage('three', [], [], {legacyIds: ['legacy-three', 'other-legacy-three']}),
|
||||
];
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output.split('\n')).toEqual([
|
||||
'{',
|
||||
' "legacy-one": "one",',
|
||||
' "other-legacy-one": "one",',
|
||||
' "legacy-two": "two",',
|
||||
' "legacy-three": "three",',
|
||||
' "other-legacy-three": "three"',
|
||||
'}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not include messages that have a custom ID', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('one', [], [], {legacyIds: ['legacy-one']}),
|
||||
mockMessage('two', [], [], {legacyIds: ['legacy-two'], customId: 'custom-two'}),
|
||||
mockMessage('three', [], [], {legacyIds: ['legacy-three']}),
|
||||
];
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output.split('\n')).toEqual([
|
||||
'{',
|
||||
' "legacy-one": "one",',
|
||||
' "legacy-three": "three"',
|
||||
'}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not include messages that do not have legacy IDs', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('one', [], [], {legacyIds: ['legacy-one']}),
|
||||
mockMessage('two', [], [], {}),
|
||||
mockMessage('three', [], [], {legacyIds: ['legacy-three']}),
|
||||
];
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output.split('\n')).toEqual([
|
||||
'{',
|
||||
' "legacy-one": "one",',
|
||||
' "legacy-three": "three"',
|
||||
'}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should produce an empty file if none of the messages need to be migrated', () => {
|
||||
const messages: ɵParsedMessage[] = [
|
||||
mockMessage('one', [], [], {legacyIds: ['legacy-one'], customId: 'custom-one'}),
|
||||
mockMessage('two', [], [], {}),
|
||||
mockMessage('three', [], [], {legacyIds: []}),
|
||||
];
|
||||
const output = serializer.serialize(messages);
|
||||
expect(output).toBe('{}');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
|
||||
|
||||
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/src/ngtsc/testing",
|
||||
"//packages/localize/src/tools",
|
||||
"//packages/localize/src/tools/test:test_lib",
|
||||
"//packages/localize/src/tools/test/helpers",
|
||||
],
|
||||
)
|
||||
|
||||
# Use copy_to_bin since filegroup doesn't seem to work on Windows.
|
||||
copy_to_bin(
|
||||
name = "test_files",
|
||||
srcs = glob(["test_files/**/*"]),
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "integration",
|
||||
bootstrap = ["//tools/testing:node_no_angular_es5"],
|
||||
data = [
|
||||
":test_files",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
"@npm//glob",
|
||||
"@npm//yargs",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,196 @@
|
|||
/**
|
||||
* @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 {MockLogger} from '@angular/compiler-cli/src/ngtsc/logging/testing';
|
||||
import {loadTestDirectory} from '@angular/compiler-cli/src/ngtsc/testing';
|
||||
import {migrateFiles} from '../../../src/migrate/main';
|
||||
import {runInNativeFileSystem} from '../../helpers';
|
||||
|
||||
runInNativeFileSystem(() => {
|
||||
let fs: FileSystem;
|
||||
let logger: MockLogger;
|
||||
let rootPath: AbsoluteFsPath;
|
||||
let mappingFilePath: AbsoluteFsPath;
|
||||
|
||||
beforeEach(() => {
|
||||
fs = getFileSystem();
|
||||
logger = new MockLogger();
|
||||
rootPath = absoluteFrom('/project');
|
||||
mappingFilePath = fs.resolve(rootPath, 'test_files/mapping.json');
|
||||
|
||||
loadTestDirectory(fs, __dirname + '/test_files', absoluteFrom('/project/test_files'));
|
||||
});
|
||||
|
||||
describe('migrateFiles()', () => {
|
||||
it('should log a warning if the migration file is empty', () => {
|
||||
const emptyMappingPath = fs.resolve(rootPath, 'test_files/empty-mapping.json');
|
||||
migrateFiles({
|
||||
rootPath,
|
||||
translationFilePaths: ['test_files/messages.json'],
|
||||
logger,
|
||||
mappingFilePath: emptyMappingPath,
|
||||
});
|
||||
|
||||
expect(logger.logs.warn).toEqual([
|
||||
[`Mapping file at ${emptyMappingPath} is empty. Either there are no messages ` +
|
||||
`that need to be migrated, or the extraction step failed to find them.`]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should migrate a json message file', () => {
|
||||
const filePath = 'test_files/messages.json';
|
||||
migrateFiles({
|
||||
rootPath,
|
||||
translationFilePaths: [filePath],
|
||||
logger,
|
||||
mappingFilePath,
|
||||
});
|
||||
|
||||
expect(readAndNormalize(fs.resolve(rootPath, filePath))).toEqual([
|
||||
`{`,
|
||||
` "locale": "en-GB",`,
|
||||
` "translations": {`,
|
||||
` "9876543": "Hello",`,
|
||||
` "custom-id": "Custom id message",`,
|
||||
` "987654321098765": "Goodbye"`,
|
||||
` }`,
|
||||
`}`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should migrate an arb message file', () => {
|
||||
const filePath = 'test_files/messages.arb';
|
||||
migrateFiles({
|
||||
rootPath,
|
||||
translationFilePaths: [filePath],
|
||||
logger,
|
||||
mappingFilePath,
|
||||
});
|
||||
expect(readAndNormalize(fs.resolve(rootPath, filePath))).toEqual([
|
||||
`{`,
|
||||
` "@@locale": "en-GB",`,
|
||||
` "9876543": "Hello",`,
|
||||
` "@9876543": {`,
|
||||
` "x-locations": [`,
|
||||
` {`,
|
||||
` "file": "test.js",`,
|
||||
` "start": { "line": "1", "column": "0" },`,
|
||||
` "end": { "line": "1", "column": "0" }`,
|
||||
` }`,
|
||||
` ]`,
|
||||
` },`,
|
||||
` "custom-id": "Custom id message",`,
|
||||
` "@custom-id": {`,
|
||||
` "x-locations": [`,
|
||||
` {`,
|
||||
` "file": "test.js",`,
|
||||
` "start": { "line": "2", "column": "0" },`,
|
||||
` "end": { "line": "2", "column": "0" }`,
|
||||
` }`,
|
||||
` ]`,
|
||||
` },`,
|
||||
` "987654321098765": "Goodbye",`,
|
||||
` "@987654321098765": {`,
|
||||
` "x-locations": [`,
|
||||
` {`,
|
||||
` "file": "test.js",`,
|
||||
` "start": { "line": "3", "column": "0" },`,
|
||||
` "end": { "line": "3", "column": "0" }`,
|
||||
` }`,
|
||||
` ]`,
|
||||
` }`,
|
||||
`}`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should migrate an xmb message file', () => {
|
||||
const filePath = 'test_files/messages.xmb';
|
||||
migrateFiles({
|
||||
rootPath,
|
||||
translationFilePaths: [filePath],
|
||||
logger,
|
||||
mappingFilePath,
|
||||
});
|
||||
expect(readAndNormalize(fs.resolve(rootPath, filePath))).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="9876543"><source>test.js:1</source>Hello</msg>`,
|
||||
` <msg id="custom-id"><source>test.js:2</source>Custom id message</msg>`,
|
||||
` <msg id="987654321098765"><source>test.js:3</source>Goodbye</msg>`,
|
||||
`</messagebundle>`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should migrate an xlf message file', () => {
|
||||
const filePath = 'test_files/messages.xlf';
|
||||
migrateFiles({
|
||||
rootPath,
|
||||
translationFilePaths: [filePath],
|
||||
logger,
|
||||
mappingFilePath,
|
||||
});
|
||||
expect(readAndNormalize(fs.resolve(rootPath, filePath))).toEqual([
|
||||
`<?xml version="1.0" encoding="UTF-8" ?>`,
|
||||
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en-GB">`,
|
||||
` <file id="ngi18n" original="ng.template" xml:space="preserve">`,
|
||||
` <unit id="9876543">`,
|
||||
` <notes>`,
|
||||
` <note category="location">test.js:1</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>Hello</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="custom-id">`,
|
||||
` <notes>`,
|
||||
` <note category="location">test.js:2</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>Custom id message</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` <unit id="987654321098765">`,
|
||||
` <notes>`,
|
||||
` <note category="location">test.js:3</note>`,
|
||||
` </notes>`,
|
||||
` <segment>`,
|
||||
` <source>Goodbye</source>`,
|
||||
` </segment>`,
|
||||
` </unit>`,
|
||||
` </file>`,
|
||||
`</xliff>`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
/** Reads a path from the file system and normalizes the line endings. */
|
||||
function readAndNormalize(path: AbsoluteFsPath): string {
|
||||
return fs.readFile(path).replace(/\r?\n/g, '\n');
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"1234567890123456789012345678901234567890": "987654321098765",
|
||||
"12345678901234567890": "9876543"
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"@@locale": "en-GB",
|
||||
"12345678901234567890": "Hello",
|
||||
"@12345678901234567890": {
|
||||
"x-locations": [
|
||||
{
|
||||
"file": "test.js",
|
||||
"start": { "line": "1", "column": "0" },
|
||||
"end": { "line": "1", "column": "0" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"custom-id": "Custom id message",
|
||||
"@custom-id": {
|
||||
"x-locations": [
|
||||
{
|
||||
"file": "test.js",
|
||||
"start": { "line": "2", "column": "0" },
|
||||
"end": { "line": "2", "column": "0" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"1234567890123456789012345678901234567890": "Goodbye",
|
||||
"@1234567890123456789012345678901234567890": {
|
||||
"x-locations": [
|
||||
{
|
||||
"file": "test.js",
|
||||
"start": { "line": "3", "column": "0" },
|
||||
"end": { "line": "3", "column": "0" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"locale": "en-GB",
|
||||
"translations": {
|
||||
"12345678901234567890": "Hello",
|
||||
"custom-id": "Custom id message",
|
||||
"1234567890123456789012345678901234567890": "Goodbye"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en-GB">
|
||||
<file id="ngi18n" original="ng.template" xml:space="preserve">
|
||||
<unit id="12345678901234567890">
|
||||
<notes>
|
||||
<note category="location">test.js:1</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>Hello</source>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="custom-id">
|
||||
<notes>
|
||||
<note category="location">test.js:2</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>Custom id message</source>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="1234567890123456789012345678901234567890">
|
||||
<notes>
|
||||
<note category="location">test.js:3</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>Goodbye</source>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
|
@ -0,0 +1,27 @@
|
|||
<?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="12345678901234567890"><source>test.js:1</source>Hello</msg>
|
||||
<msg id="custom-id"><source>test.js:2</source>Custom id message</msg>
|
||||
<msg id="1234567890123456789012345678901234567890"><source>test.js:3</source>Goodbye</msg>
|
||||
</messagebundle>
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* @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 {migrateFile} from '../../src/migrate/migrate';
|
||||
|
||||
describe('migrateFile', () => {
|
||||
it('should migrate all of the legacy message IDs', () => {
|
||||
const source = `
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="fr-FR" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="123hello-legacy" datatype="html">
|
||||
<source>Hello</source>
|
||||
<target>Bonjour</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="456goodbye-legacy" datatype="html">
|
||||
<source>Goodbye</source>
|
||||
<target>Au revoir</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
`;
|
||||
|
||||
const result = migrateFile(source, {
|
||||
'123hello-legacy': 'hello-migrated',
|
||||
'456goodbye-legacy': 'goodbye-migrated',
|
||||
});
|
||||
|
||||
expect(result).toContain('<trans-unit id="hello-migrated" datatype="html">');
|
||||
expect(result).toContain('<trans-unit id="goodbye-migrated" datatype="html">');
|
||||
});
|
||||
|
||||
it('should migrate messages whose ID contains special regex characters', () => {
|
||||
const source = `
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="fr-FR" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="123hello(.*legacy" datatype="html">
|
||||
<source>Hello</source>
|
||||
<target>Bonjour</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
`;
|
||||
|
||||
const result = migrateFile(source, {'123hello(.*legacy': 'hello-migrated'});
|
||||
expect(result).toContain('<trans-unit id="hello-migrated" datatype="html">');
|
||||
});
|
||||
|
||||
it('should not migrate messages that are not in the mapping', () => {
|
||||
const source = `
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="fr-FR" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="123hello-legacy" datatype="html">
|
||||
<source>Hello</source>
|
||||
<target>Bonjour</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="456goodbye" datatype="html">
|
||||
<source>Goodbye</source>
|
||||
<target>Au revoir</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
`;
|
||||
|
||||
const result = migrateFile(source, {'123hello-legacy': 'hello-migrated'});
|
||||
expect(result).toContain('<trans-unit id="hello-migrated" datatype="html">');
|
||||
expect(result).toContain('<trans-unit id="456goodbye" datatype="html">');
|
||||
});
|
||||
|
||||
it('should not modify the file if none of the mappings match', () => {
|
||||
const source = `
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="fr-FR" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="123hello-legacy" datatype="html">
|
||||
<source>Hello</source>
|
||||
<target>Bonjour</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="456goodbye-legacy" datatype="html">
|
||||
<source>Goodbye</source>
|
||||
<target>Au revoir</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
`;
|
||||
|
||||
const result = migrateFile(source, {
|
||||
'does-not-match': 'migrated-does-not-match',
|
||||
'also-does-not-match': 'migrated-also-does-not-match',
|
||||
});
|
||||
|
||||
expect(result).toBe(source);
|
||||
});
|
||||
|
||||
// Note: it shouldn't be possible for the ID to be repeated multiple times, but
|
||||
// this assertion is here to make sure that it behaves as expected if it does.
|
||||
it('should migrate if an ID appears in more than one place', () => {
|
||||
const source = `
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="fr-FR" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="123hello-legacy" datatype="html">
|
||||
<source>Hello</source>
|
||||
<target>Bonjour</target>
|
||||
</trans-unit>
|
||||
|
||||
<made-up-tag datatype="html" belongs-to="123hello-legacy">
|
||||
<source id="123hello-legacy">Hello</source>
|
||||
<target target-id="123hello-legacy">Bonjour</target>
|
||||
</mage-up-tag>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
`;
|
||||
|
||||
const result = migrateFile(source, {'123hello-legacy': 'hello-migrated'});
|
||||
expect(result).toContain('<trans-unit id="hello-migrated" datatype="html">');
|
||||
expect(result).toContain('<made-up-tag datatype="html" belongs-to="hello-migrated">');
|
||||
expect(result).toContain('<source id="hello-migrated">');
|
||||
expect(result).toContain('<target target-id="hello-migrated">Bonjour</target>');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue