From 17354304768f3c2b272c4c5d5636b5709287276f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sat, 27 Feb 2021 14:49:34 +0100 Subject: [PATCH] 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 --- packages/localize/package.json | 3 +- .../localize/src/tools/src/extract/main.ts | 24 ++- .../legacy_message_id_migration_serializer.ts | 50 +++++ .../localize/src/tools/src/migrate/main.ts | 94 +++++++++ .../localize/src/tools/src/migrate/migrate.ts | 30 +++ .../test/extract/integration/main_spec.ts | 43 ++++ ...cy_message_id_migration_serializer_spec.ts | 79 +++++++ .../test/migrate/integration/BUILD.bazel | 40 ++++ .../test/migrate/integration/main_spec.ts | 196 ++++++++++++++++++ .../integration/test_files/empty-mapping.json | 1 + .../integration/test_files/mapping.json | 4 + .../integration/test_files/messages.arb | 33 +++ .../integration/test_files/messages.json | 8 + .../integration/test_files/messages.xlf | 29 +++ .../integration/test_files/messages.xmb | 27 +++ .../src/tools/test/migrate/migrate_spec.ts | 136 ++++++++++++ 16 files changed, 787 insertions(+), 10 deletions(-) create mode 100644 packages/localize/src/tools/src/extract/translation_files/legacy_message_id_migration_serializer.ts create mode 100644 packages/localize/src/tools/src/migrate/main.ts create mode 100644 packages/localize/src/tools/src/migrate/migrate.ts create mode 100644 packages/localize/src/tools/test/extract/translation_files/legacy_message_id_migration_serializer_spec.ts create mode 100644 packages/localize/src/tools/test/migrate/integration/BUILD.bazel create mode 100644 packages/localize/src/tools/test/migrate/integration/main_spec.ts create mode 100644 packages/localize/src/tools/test/migrate/integration/test_files/empty-mapping.json create mode 100644 packages/localize/src/tools/test/migrate/integration/test_files/mapping.json create mode 100644 packages/localize/src/tools/test/migrate/integration/test_files/messages.arb create mode 100644 packages/localize/src/tools/test/migrate/integration/test_files/messages.json create mode 100644 packages/localize/src/tools/test/migrate/integration/test_files/messages.xlf create mode 100644 packages/localize/src/tools/test/migrate/integration/test_files/messages.xmb create mode 100644 packages/localize/src/tools/test/migrate/migrate_spec.ts diff --git a/packages/localize/package.json b/packages/localize/package.json index 856cdf3b01..ed947cb335 100644 --- a/packages/localize/package.json +++ b/packages/localize/package.json @@ -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", diff --git a/packages/localize/src/tools/src/extract/main.ts b/packages/localize/src/tools/src/extract/main.ts index a19c2ae458..ee1bd2d1cd 100644 --- a/packages/localize/src/tools/src/extract/main.ts +++ b/packages/localize/src/tools/src/extract/main.ts @@ -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}`); } diff --git a/packages/localize/src/tools/src/extract/translation_files/legacy_message_id_migration_serializer.ts b/packages/localize/src/tools/src/extract/translation_files/legacy_message_id_migration_serializer.ts new file mode 100644 index 0000000000..601cba08ce --- /dev/null +++ b/packages/localize/src/tools/src/extract/translation_files/legacy_message_id_migration_serializer.ts @@ -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); + + 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; +} diff --git a/packages/localize/src/tools/src/migrate/main.ts b/packages/localize/src/tools/src/migrate/main.ts new file mode 100644 index 0000000000..266131ce60 --- /dev/null +++ b/packages/localize/src/tools/src/migrate/main.ts @@ -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)); + }); + } +} diff --git a/packages/localize/src/tools/src/migrate/migrate.ts b/packages/localize/src/tools/src/migrate/migrate.ts new file mode 100644 index 0000000000..4cf269f390 --- /dev/null +++ b/packages/localize/src/tools/src/migrate/migrate.ts @@ -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'); +} diff --git a/packages/localize/src/tools/test/extract/integration/main_spec.ts b/packages/localize/src/tools/test/extract/integration/main_spec.ts index 982ee27210..00b84e43a2 100644 --- a/packages/localize/src/tools/test/extract/integration/main_spec.ts +++ b/packages/localize/src/tools/test/extract/integration/main_spec.ts @@ -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.' + ]]); + }); }); }); }); diff --git a/packages/localize/src/tools/test/extract/translation_files/legacy_message_id_migration_serializer_spec.ts b/packages/localize/src/tools/test/extract/translation_files/legacy_message_id_migration_serializer_spec.ts new file mode 100644 index 0000000000..3c284d9a98 --- /dev/null +++ b/packages/localize/src/tools/test/extract/translation_files/legacy_message_id_migration_serializer_spec.ts @@ -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('{}'); + }); +}); diff --git a/packages/localize/src/tools/test/migrate/integration/BUILD.bazel b/packages/localize/src/tools/test/migrate/integration/BUILD.bazel new file mode 100644 index 0000000000..1b3e056fec --- /dev/null +++ b/packages/localize/src/tools/test/migrate/integration/BUILD.bazel @@ -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", + ], +) diff --git a/packages/localize/src/tools/test/migrate/integration/main_spec.ts b/packages/localize/src/tools/test/migrate/integration/main_spec.ts new file mode 100644 index 0000000000..9972cd4df9 --- /dev/null +++ b/packages/localize/src/tools/test/migrate/integration/main_spec.ts @@ -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([ + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + `]>`, + ``, + ` test.js:1Hello`, + ` test.js:2Custom id message`, + ` test.js:3Goodbye`, + ``, + ].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([ + ``, + ``, + ` `, + ` `, + ` `, + ` test.js:1`, + ` `, + ` `, + ` Hello`, + ` `, + ` `, + ` `, + ` `, + ` test.js:2`, + ` `, + ` `, + ` Custom id message`, + ` `, + ` `, + ` `, + ` `, + ` test.js:3`, + ` `, + ` `, + ` Goodbye`, + ` `, + ` `, + ` `, + ``, + ].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'); + } + }); +}); diff --git a/packages/localize/src/tools/test/migrate/integration/test_files/empty-mapping.json b/packages/localize/src/tools/test/migrate/integration/test_files/empty-mapping.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/localize/src/tools/test/migrate/integration/test_files/empty-mapping.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/localize/src/tools/test/migrate/integration/test_files/mapping.json b/packages/localize/src/tools/test/migrate/integration/test_files/mapping.json new file mode 100644 index 0000000000..4676f97881 --- /dev/null +++ b/packages/localize/src/tools/test/migrate/integration/test_files/mapping.json @@ -0,0 +1,4 @@ +{ + "1234567890123456789012345678901234567890": "987654321098765", + "12345678901234567890": "9876543" +} \ No newline at end of file diff --git a/packages/localize/src/tools/test/migrate/integration/test_files/messages.arb b/packages/localize/src/tools/test/migrate/integration/test_files/messages.arb new file mode 100644 index 0000000000..7eb93a730f --- /dev/null +++ b/packages/localize/src/tools/test/migrate/integration/test_files/messages.arb @@ -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" } + } + ] + } +} \ No newline at end of file diff --git a/packages/localize/src/tools/test/migrate/integration/test_files/messages.json b/packages/localize/src/tools/test/migrate/integration/test_files/messages.json new file mode 100644 index 0000000000..deaaf2030f --- /dev/null +++ b/packages/localize/src/tools/test/migrate/integration/test_files/messages.json @@ -0,0 +1,8 @@ +{ + "locale": "en-GB", + "translations": { + "12345678901234567890": "Hello", + "custom-id": "Custom id message", + "1234567890123456789012345678901234567890": "Goodbye" + } +} \ No newline at end of file diff --git a/packages/localize/src/tools/test/migrate/integration/test_files/messages.xlf b/packages/localize/src/tools/test/migrate/integration/test_files/messages.xlf new file mode 100644 index 0000000000..c0afe96041 --- /dev/null +++ b/packages/localize/src/tools/test/migrate/integration/test_files/messages.xlf @@ -0,0 +1,29 @@ + + + + + + test.js:1 + + + Hello + + + + + test.js:2 + + + Custom id message + + + + + test.js:3 + + + Goodbye + + + + \ No newline at end of file diff --git a/packages/localize/src/tools/test/migrate/integration/test_files/messages.xmb b/packages/localize/src/tools/test/migrate/integration/test_files/messages.xmb new file mode 100644 index 0000000000..87bc4cace0 --- /dev/null +++ b/packages/localize/src/tools/test/migrate/integration/test_files/messages.xmb @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + +]> + + test.js:1Hello + test.js:2Custom id message + test.js:3Goodbye + \ No newline at end of file diff --git a/packages/localize/src/tools/test/migrate/migrate_spec.ts b/packages/localize/src/tools/test/migrate/migrate_spec.ts new file mode 100644 index 0000000000..46f55fda2a --- /dev/null +++ b/packages/localize/src/tools/test/migrate/migrate_spec.ts @@ -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 = ` + + + + + Hello + Bonjour + + + + Goodbye + Au revoir + + + + + `; + + const result = migrateFile(source, { + '123hello-legacy': 'hello-migrated', + '456goodbye-legacy': 'goodbye-migrated', + }); + + expect(result).toContain(''); + expect(result).toContain(''); + }); + + it('should migrate messages whose ID contains special regex characters', () => { + const source = ` + + + + + Hello + Bonjour + + + + + `; + + const result = migrateFile(source, {'123hello(.*legacy': 'hello-migrated'}); + expect(result).toContain(''); + }); + + it('should not migrate messages that are not in the mapping', () => { + const source = ` + + + + + Hello + Bonjour + + + + Goodbye + Au revoir + + + + + `; + + const result = migrateFile(source, {'123hello-legacy': 'hello-migrated'}); + expect(result).toContain(''); + expect(result).toContain(''); + }); + + it('should not modify the file if none of the mappings match', () => { + const source = ` + + + + + Hello + Bonjour + + + + Goodbye + Au revoir + + + + + `; + + 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 = ` + + + + + Hello + Bonjour + + + + Hello + Bonjour + + + + + `; + + const result = migrateFile(source, {'123hello-legacy': 'hello-migrated'}); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('Bonjour'); + }); +});