From 141fcb95a4f35ca0b16549b5c38c45f071b1cc61 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 28 Apr 2020 21:03:27 +0100 Subject: [PATCH] refactor(localize): use the `FileSystem` from ngtsc (#36843) This commit makes the leap from its own custom baked `FileUtils` solution to the fully formed `FileSystem` that is used in the compiler-cli. This makes testing more straightforward and helps to ensure that the tool will work across operatings systems. Also, going forward, it will allow the localize project access to other useful code from the compiler-cli, such as source-map handling. PR Close #36843 --- packages/localize/package.json | 10 +- packages/localize/src/tools/BUILD.bazel | 1 + packages/localize/src/tools/src/file_utils.ts | 52 --- .../asset_files/asset_translation_handler.ts | 38 +- .../localize/src/tools/src/translate/main.ts | 37 +- .../src/tools/src/translate/output_path.ts | 7 +- .../source_file_translation_handler.ts | 46 +- .../translation_files/translation_loader.ts | 17 +- .../src/tools/src/translate/translator.ts | 21 +- packages/localize/src/tools/test/BUILD.bazel | 2 + .../asset_file_translation_handler_spec.ts | 92 ++-- .../test/translate/integration/BUILD.bazel | 3 + .../test/translate/integration/main_spec.ts | 366 ++++++++-------- .../tools/test/translate/output_path_spec.ts | 66 ++- .../source_file_translation_handler_spec.ts | 221 +++++----- .../translation_loader_spec.ts | 399 +++++++++--------- .../tools/test/translate/translator_spec.ts | 192 +++++---- 17 files changed, 816 insertions(+), 754 deletions(-) delete mode 100644 packages/localize/src/tools/src/file_utils.ts diff --git a/packages/localize/package.json b/packages/localize/package.json index 53dc3de304..27aa1dd296 100644 --- a/packages/localize/package.json +++ b/packages/localize/package.json @@ -28,7 +28,11 @@ "glob": "7.1.2", "yargs": "15.3.0" }, - "publishConfig":{ - "registry":"https://wombat-dressing-room.appspot.com" + "peerDependencies": { + "@angular/compiler": "0.0.0-PLACEHOLDER", + "@angular/compiler-cli": "0.0.0-PLACEHOLDER" + }, + "publishConfig": { + "registry": "https://wombat-dressing-room.appspot.com" } -} +} \ No newline at end of file diff --git a/packages/localize/src/tools/BUILD.bazel b/packages/localize/src/tools/BUILD.bazel index daf193f862..6f37b1b7c2 100644 --- a/packages/localize/src/tools/BUILD.bazel +++ b/packages/localize/src/tools/BUILD.bazel @@ -19,6 +19,7 @@ ts_library( tsconfig = ":tsconfig", deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/localize", "@npm//@babel/core", "@npm//@babel/types", diff --git a/packages/localize/src/tools/src/file_utils.ts b/packages/localize/src/tools/src/file_utils.ts deleted file mode 100644 index d34f57abcc..0000000000 --- a/packages/localize/src/tools/src/file_utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 * as fs from 'fs'; -import * as path from 'path'; - -export class FileUtils { - static readFile(absolutePath: string): string { - return fs.readFileSync(absolutePath, 'utf8'); - } - - static readFileBuffer(absolutePath: string): Buffer { - return fs.readFileSync(absolutePath); - } - - static writeFile(absolutePath: string, contents: string|Buffer) { - FileUtils.ensureDir(path.dirname(absolutePath)); - fs.writeFileSync(absolutePath, contents); - } - - static ensureDir(absolutePath: string): void { - const parents: string[] = []; - while (!FileUtils.isRoot(absolutePath) && !fs.existsSync(absolutePath)) { - parents.push(absolutePath); - absolutePath = path.dirname(absolutePath); - } - while (parents.length) { - fs.mkdirSync(parents.pop()!); - } - } - - static remove(p: string): void { - const stat = fs.statSync(p); - if (stat.isFile()) { - fs.unlinkSync(p); - } else if (stat.isDirectory()) { - fs.readdirSync(p).forEach(child => { - const absChild = path.resolve(p, child); - FileUtils.remove(absChild); - }); - fs.rmdirSync(p); - } - } - - static isRoot(absolutePath: string): boolean { - return path.dirname(absolutePath) === absolutePath; - } -} diff --git a/packages/localize/src/tools/src/translate/asset_files/asset_translation_handler.ts b/packages/localize/src/tools/src/translate/asset_files/asset_translation_handler.ts index ae70ba7c86..0f4d5293cc 100644 --- a/packages/localize/src/tools/src/translate/asset_files/asset_translation_handler.ts +++ b/packages/localize/src/tools/src/translate/asset_files/asset_translation_handler.ts @@ -5,8 +5,9 @@ * 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, PathSegment} from '@angular/compiler-cli/src/ngtsc/file_system'; + import {Diagnostics} from '../../diagnostics'; -import {FileUtils} from '../../file_utils'; import {OutputPathFn} from '../output_path'; import {TranslationBundle, TranslationHandler} from '../translator'; @@ -14,25 +15,34 @@ import {TranslationBundle, TranslationHandler} from '../translator'; * Translate an asset file by simply copying it to the appropriate translation output paths. */ export class AssetTranslationHandler implements TranslationHandler { - canTranslate(_relativeFilePath: string, _contents: Buffer): boolean { + constructor(private fs: FileSystem) {} + + canTranslate(_relativeFilePath: PathSegment, _contents: Buffer): boolean { return true; } + translate( - diagnostics: Diagnostics, _sourceRoot: string, relativeFilePath: string, contents: Buffer, - outputPathFn: OutputPathFn, translations: TranslationBundle[], sourceLocale?: string): void { + diagnostics: Diagnostics, _sourceRoot: AbsoluteFsPath, relativeFilePath: PathSegment, + contents: Buffer, outputPathFn: OutputPathFn, translations: TranslationBundle[], + sourceLocale?: string): void { for (const translation of translations) { - try { - FileUtils.writeFile(outputPathFn(translation.locale, relativeFilePath), contents); - } catch (e) { - diagnostics.error(e.message); - } + this.writeAssetFile( + diagnostics, outputPathFn, translation.locale, relativeFilePath, contents); } if (sourceLocale !== undefined) { - try { - FileUtils.writeFile(outputPathFn(sourceLocale, relativeFilePath), contents); - } catch (e) { - diagnostics.error(e.message); - } + this.writeAssetFile(diagnostics, outputPathFn, sourceLocale, relativeFilePath, contents); + } + } + + private writeAssetFile( + diagnostics: Diagnostics, outputPathFn: OutputPathFn, locale: string, + relativeFilePath: PathSegment, contents: Buffer): void { + try { + const outputPath = absoluteFrom(outputPathFn(locale, relativeFilePath)); + this.fs.ensureDir(this.fs.dirname(outputPath)); + this.fs.writeFile(outputPath, contents); + } catch (e) { + diagnostics.error(e.message); } } } diff --git a/packages/localize/src/tools/src/translate/main.ts b/packages/localize/src/tools/src/translate/main.ts index 9178ed810d..0cfcc4cd98 100644 --- a/packages/localize/src/tools/src/translate/main.ts +++ b/packages/localize/src/tools/src/translate/main.ts @@ -6,8 +6,8 @@ * 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, relativeFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import * as glob from 'glob'; -import {resolve} from 'path'; import * as yargs from 'yargs'; import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics'; @@ -66,8 +66,9 @@ if (require.main === module) { .option('o', { alias: 'outputPath', required: true, - describe: - 'A output path pattern to where the translated files will be written. The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.' + describe: 'A output path pattern to where the translated files will be written.\n' + + 'The path must be either absolute or relative to the current working directory.\n' + + 'The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.' }) .option('m', { @@ -88,11 +89,13 @@ if (require.main === module) { .help() .parse(args); + const fs = new NodeJSFileSystem(); + setFileSystem(fs); + const sourceRootPath = options['r']; - const sourceFilePaths = - glob.sync(options['s'], {absolute: true, cwd: sourceRootPath, nodir: true}); + const sourceFilePaths = glob.sync(options['s'], {cwd: sourceRootPath, nodir: true}); const translationFilePaths: (string|string[])[] = convertArraysFromArgs(options['t']); - const outputPathFn = getOutputPathFn(options['o']); + const outputPathFn = getOutputPathFn(fs.resolve(options['o'])); const diagnostics = new Diagnostics(); const missingTranslation: DiagnosticHandlingStrategy = options['m']; const duplicateTranslation: DiagnosticHandlingStrategy = options['d']; @@ -154,8 +157,9 @@ export interface TranslateFilesOptions { */ translationFileLocales: (string|undefined)[]; /** - * A function that computes the output path of where the translated files will be written. - * The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`. + * A function that computes the output path of where the translated files will be + * written. The marker `{{LOCALE}}` will be replaced with the target locale. E.g. + * `dist/{{LOCALE}}`. */ outputPathFn: OutputPathFn; /** @@ -189,7 +193,9 @@ export function translateFiles({ duplicateTranslation, sourceLocale }: TranslateFilesOptions) { + const fs = getFileSystem(); const translationLoader = new TranslationLoader( + fs, [ new Xliff2TranslationParser(), new Xliff1TranslationParser(), @@ -199,21 +205,24 @@ export function translateFiles({ duplicateTranslation, diagnostics); const resourceProcessor = new Translator( + fs, [ - new SourceFileTranslationHandler({missingTranslation}), - new AssetTranslationHandler(), + new SourceFileTranslationHandler(fs, {missingTranslation}), + new AssetTranslationHandler(fs), ], diagnostics); // Convert all the `translationFilePaths` elements to arrays. - const translationFilePathsArrays = - translationFilePaths.map(filePaths => Array.isArray(filePaths) ? filePaths : [filePaths]); + const translationFilePathsArrays = translationFilePaths.map( + filePaths => + Array.isArray(filePaths) ? filePaths.map(p => fs.resolve(p)) : [fs.resolve(filePaths)]); const translations = translationLoader.loadBundles(translationFilePathsArrays, translationFileLocales); - sourceRootPath = resolve(sourceRootPath); + sourceRootPath = fs.resolve(sourceRootPath); resourceProcessor.translateFiles( - sourceFilePaths, sourceRootPath, outputPathFn, translations, sourceLocale); + sourceFilePaths.map(relativeFrom), fs.resolve(sourceRootPath), outputPathFn, translations, + sourceLocale); } /** diff --git a/packages/localize/src/tools/src/translate/output_path.ts b/packages/localize/src/tools/src/translate/output_path.ts index f33a65c5ce..28ee6750c3 100644 --- a/packages/localize/src/tools/src/translate/output_path.ts +++ b/packages/localize/src/tools/src/translate/output_path.ts @@ -5,8 +5,13 @@ * 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} from '@angular/compiler-cli/src/ngtsc/file_system'; import {join} from 'path'; +/** + * A function that will return an absolute path to where a file is to be written, given a locale and + * a relative path. + */ export interface OutputPathFn { (locale: string, relativePath: string): string; } @@ -18,7 +23,7 @@ export interface OutputPathFn { * The special `{{LOCALE}}` marker will be replaced with the locale code of the current translation. * @param outputFolder An absolute path to the folder containing this set of translations. */ -export function getOutputPathFn(outputFolder: string): OutputPathFn { +export function getOutputPathFn(outputFolder: AbsoluteFsPath): OutputPathFn { const [pre, post] = outputFolder.split('{{LOCALE}}'); return post === undefined ? (_locale, relativePath) => join(pre, relativePath) : (locale, relativePath) => join(pre + locale + post, relativePath); diff --git a/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts b/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts index 60b93dba96..e481092386 100644 --- a/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts +++ b/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts @@ -5,14 +5,15 @@ * 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, PathSegment, relativeFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {parseSync, transformFromAstSync} from '@babel/core'; import {File, Program} from '@babel/types'; -import {extname, join} from 'path'; + import {Diagnostics} from '../../diagnostics'; -import {FileUtils} from '../../file_utils'; import {TranslatePluginOptions} from '../../source_file_utils'; import {OutputPathFn} from '../output_path'; import {TranslationBundle, TranslationHandler} from '../translator'; + import {makeEs2015TranslatePlugin} from './es2015_translate_plugin'; import {makeEs5TranslatePlugin} from './es5_translate_plugin'; import {makeLocalePlugin} from './locale_plugin'; @@ -24,29 +25,32 @@ import {makeLocalePlugin} from './locale_plugin'; export class SourceFileTranslationHandler implements TranslationHandler { private sourceLocaleOptions: TranslatePluginOptions = {...this.translationOptions, missingTranslation: 'ignore'}; - constructor(private translationOptions: TranslatePluginOptions = {}) {} + constructor(private fs: FileSystem, private translationOptions: TranslatePluginOptions = {}) {} - canTranslate(relativeFilePath: string, _contents: Buffer): boolean { - return extname(relativeFilePath) === '.js'; + canTranslate(relativeFilePath: PathSegment, _contents: Buffer): boolean { + return this.fs.extname(relativeFrom(relativeFilePath)) === '.js'; } translate( - diagnostics: Diagnostics, sourceRoot: string, relativeFilePath: string, contents: Buffer, - outputPathFn: OutputPathFn, translations: TranslationBundle[], sourceLocale?: string): void { + diagnostics: Diagnostics, sourceRoot: AbsoluteFsPath, relativeFilePath: PathSegment, + contents: Buffer, outputPathFn: OutputPathFn, translations: TranslationBundle[], + sourceLocale?: string): void { const sourceCode = contents.toString('utf8'); // A short-circuit check to avoid parsing the file into an AST if it does not contain any // `$localize` identifiers. if (!sourceCode.includes('$localize')) { for (const translation of translations) { - FileUtils.writeFile(outputPathFn(translation.locale, relativeFilePath), contents); + this.writeSourceFile( + diagnostics, outputPathFn, translation.locale, relativeFilePath, contents); } if (sourceLocale !== undefined) { - FileUtils.writeFile(outputPathFn(sourceLocale, relativeFilePath), contents); + this.writeSourceFile(diagnostics, outputPathFn, sourceLocale, relativeFilePath, contents); } } else { const ast = parseSync(sourceCode, {sourceRoot, filename: relativeFilePath}); if (!ast) { - diagnostics.error(`Unable to parse source file: ${join(sourceRoot, relativeFilePath)}`); + diagnostics.error( + `Unable to parse source file: ${this.fs.join(sourceRoot, relativeFilePath)}`); return; } // Output a translated copy of the file for each locale. @@ -67,7 +71,7 @@ export class SourceFileTranslationHandler implements TranslationHandler { private translateFile( diagnostics: Diagnostics, ast: File|Program, translationBundle: TranslationBundle, - sourceRoot: string, filename: string, outputPathFn: OutputPathFn, + sourceRoot: AbsoluteFsPath, filename: PathSegment, outputPathFn: OutputPathFn, options: TranslatePluginOptions) { const translated = transformFromAstSync(ast, undefined, { compact: true, @@ -80,10 +84,26 @@ export class SourceFileTranslationHandler implements TranslationHandler { filename, }); if (translated && translated.code) { - FileUtils.writeFile(outputPathFn(translationBundle.locale, filename), translated.code); + this.writeSourceFile( + diagnostics, outputPathFn, translationBundle.locale, filename, translated.code); + const outputPath = absoluteFrom(outputPathFn(translationBundle.locale, filename)); + this.fs.ensureDir(this.fs.dirname(outputPath)); + this.fs.writeFile(outputPath, translated.code); } else { - diagnostics.error(`Unable to translate source file: ${join(sourceRoot, filename)}`); + diagnostics.error(`Unable to translate source file: ${this.fs.join(sourceRoot, filename)}`); return; } } + + private writeSourceFile( + diagnostics: Diagnostics, outputPathFn: OutputPathFn, locale: string, + relativeFilePath: PathSegment, contents: string|Buffer): void { + try { + const outputPath = absoluteFrom(outputPathFn(locale, relativeFilePath)); + this.fs.ensureDir(this.fs.dirname(outputPath)); + this.fs.writeFile(outputPath, contents); + } catch (e) { + diagnostics.error(e.message); + } + } } diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts index a7cf016f82..7524074c38 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts @@ -5,8 +5,8 @@ * 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} from '@angular/compiler-cli/src/ngtsc/file_system'; import {DiagnosticHandlingStrategy, Diagnostics} from '../../diagnostics'; -import {FileUtils} from '../../file_utils'; import {TranslationBundle} from '../translator'; import {TranslationParser} from './translation_parsers/translation_parser'; @@ -16,7 +16,7 @@ import {TranslationParser} from './translation_parsers/translation_parser'; */ export class TranslationLoader { constructor( - private translationParsers: TranslationParser[], + private fs: FileSystem, private translationParsers: TranslationParser[], private duplicateTranslation: DiagnosticHandlingStrategy, /** @deprecated */ private diagnostics?: Diagnostics) {} @@ -42,8 +42,9 @@ export class TranslationLoader { * If there are both a provided locale and a locale parsed from the file, and they are not the * same, then a warning is reported. */ - loadBundles(translationFilePaths: string[][], translationFileLocales: (string|undefined)[]): - TranslationBundle[] { + loadBundles( + translationFilePaths: AbsoluteFsPath[][], + translationFileLocales: (string|undefined)[]): TranslationBundle[] { return translationFilePaths.map((filePaths, index) => { const providedLocale = translationFileLocales[index]; return this.mergeBundles(filePaths, providedLocale); @@ -53,8 +54,9 @@ export class TranslationLoader { /** * Load all the translations from the file at the given `filePath`. */ - private loadBundle(filePath: string, providedLocale: string|undefined): TranslationBundle { - const fileContents = FileUtils.readFile(filePath); + private loadBundle(filePath: AbsoluteFsPath, providedLocale: string|undefined): + TranslationBundle { + const fileContents = this.fs.readFile(filePath); for (const translationParser of this.translationParsers) { const result = translationParser.canParse(filePath, fileContents); if (!result) { @@ -96,7 +98,8 @@ export class TranslationLoader { * There is more than one `filePath` for this locale, so load each as a bundle and then merge them * all together. */ - private mergeBundles(filePaths: string[], providedLocale: string|undefined): TranslationBundle { + private mergeBundles(filePaths: AbsoluteFsPath[], providedLocale: string|undefined): + TranslationBundle { const bundles = filePaths.map(filePath => this.loadBundle(filePath, providedLocale)); const bundle = bundles[0]; for (let i = 1; i < bundles.length; i++) { diff --git a/packages/localize/src/tools/src/translate/translator.ts b/packages/localize/src/tools/src/translate/translator.ts index 74ccebada5..aff3efc726 100644 --- a/packages/localize/src/tools/src/translate/translator.ts +++ b/packages/localize/src/tools/src/translate/translator.ts @@ -5,11 +5,10 @@ * 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 {ɵMessageId, ɵParsedTranslation} from '@angular/localize'; -import {relative} from 'path'; import {Diagnostics} from '../diagnostics'; -import {FileUtils} from '../file_utils'; import {OutputPathFn} from './output_path'; @@ -36,7 +35,7 @@ export interface TranslationHandler { * @param relativeFilePath A relative path from the sourceRoot to the resource file to handle. * @param contents The contents of the file to handle. */ - canTranslate(relativeFilePath: string, contents: Buffer): boolean; + canTranslate(relativeFilePath: PathSegment, contents: Buffer): boolean; /** * Translate the file at `relativeFilePath` containing `contents`, using the given `translations`, @@ -54,8 +53,9 @@ export interface TranslationHandler { * stripped out. */ translate( - diagnostics: Diagnostics, sourceRoot: string, relativeFilePath: string, contents: Buffer, - outputPathFn: OutputPathFn, translations: TranslationBundle[], sourceLocale?: string): void; + diagnostics: Diagnostics, sourceRoot: AbsoluteFsPath, relativeFilePath: PathSegment, + contents: Buffer, outputPathFn: OutputPathFn, translations: TranslationBundle[], + sourceLocale?: string): void; } /** @@ -63,14 +63,17 @@ export interface TranslationHandler { * The file will be translated by the first handler that returns true for `canTranslate()`. */ export class Translator { - constructor(private resourceHandlers: TranslationHandler[], private diagnostics: Diagnostics) {} + constructor( + private fs: FileSystem, private resourceHandlers: TranslationHandler[], + private diagnostics: Diagnostics) {} translateFiles( - inputPaths: string[], rootPath: string, outputPathFn: OutputPathFn, + inputPaths: PathSegment[], rootPath: AbsoluteFsPath, outputPathFn: OutputPathFn, translations: TranslationBundle[], sourceLocale?: string): void { inputPaths.forEach(inputPath => { - const contents = FileUtils.readFileBuffer(inputPath); - const relativePath = relative(rootPath, inputPath); + const absInputPath = this.fs.resolve(rootPath, inputPath); + const contents = this.fs.readFileBuffer(absInputPath); + const relativePath = this.fs.relative(rootPath, absInputPath); for (const resourceHandler of this.resourceHandlers) { if (resourceHandler.canTranslate(relativePath, contents)) { return resourceHandler.translate( diff --git a/packages/localize/src/tools/test/BUILD.bazel b/packages/localize/src/tools/test/BUILD.bazel index b828ba9ec8..0ebf350f39 100644 --- a/packages/localize/src/tools/test/BUILD.bazel +++ b/packages/localize/src/tools/test/BUILD.bazel @@ -9,6 +9,8 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/localize", "//packages/localize/src/tools", "@npm//@babel/core", diff --git a/packages/localize/src/tools/test/translate/asset_files/asset_file_translation_handler_spec.ts b/packages/localize/src/tools/test/translate/asset_files/asset_file_translation_handler_spec.ts index 791656729d..22ee8920cd 100644 --- a/packages/localize/src/tools/test/translate/asset_files/asset_file_translation_handler_spec.ts +++ b/packages/localize/src/tools/test/translate/asset_files/asset_file_translation_handler_spec.ts @@ -5,56 +5,70 @@ * 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, PathSegment, relativeFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; + import {Diagnostics} from '../../../src/diagnostics'; -import {FileUtils} from '../../../src/file_utils'; import {AssetTranslationHandler} from '../../../src/translate/asset_files/asset_translation_handler'; import {TranslationBundle} from '../../../src/translate/translator'; -describe('AssetTranslationHandler', () => { - describe('canTranslate()', () => { - it('should always return true', () => { - const handler = new AssetTranslationHandler(); - expect(handler.canTranslate('relative/path', Buffer.from('contents'))).toBe(true); - }); - }); +runInEachFileSystem(() => { + describe('AssetTranslationHandler', () => { + let fs: FileSystem; + let rootPath: AbsoluteFsPath; + let filePath: PathSegment; + let enTranslationPath: AbsoluteFsPath; + let enUSTranslationPath: AbsoluteFsPath; + let frTranslationPath: AbsoluteFsPath; - describe('translate()', () => { beforeEach(() => { - spyOn(FileUtils, 'writeFile'); - spyOn(FileUtils, 'ensureDir'); + fs = getFileSystem(); + rootPath = absoluteFrom('/root/path'); + filePath = relativeFrom('relative/path'); + enTranslationPath = absoluteFrom('/translations/en/relative/path'); + enUSTranslationPath = absoluteFrom('/translations/en-US/relative/path'); + frTranslationPath = absoluteFrom('/translations/fr/relative/path'); }); - it('should write the translated file for each translation locale', () => { - const diagnostics = new Diagnostics(); - const handler = new AssetTranslationHandler(); - const translations = [ - {locale: 'en', translations: {}}, - {locale: 'fr', translations: {}}, - ]; - const contents = Buffer.from('contents'); - handler.translate( - diagnostics, '/root/path', 'relative/path', contents, mockOutputPathFn, translations); - - expect(FileUtils.writeFile).toHaveBeenCalledWith('/translations/en/relative/path', contents); - expect(FileUtils.writeFile).toHaveBeenCalledWith('/translations/fr/relative/path', contents); + describe('canTranslate()', () => { + it('should always return true', () => { + const handler = new AssetTranslationHandler(fs); + expect(handler.canTranslate(filePath, Buffer.from('contents'))).toBe(true); + }); }); - it('should write the translated file to the source locale if provided', () => { - const diagnostics = new Diagnostics(); - const handler = new AssetTranslationHandler(); - const translations: TranslationBundle[] = []; - const contents = Buffer.from('contents'); - const sourceLocale = 'en-US'; - handler.translate( - diagnostics, '/root/path', 'relative/path', contents, mockOutputPathFn, translations, - sourceLocale); + describe('translate()', () => { + it('should write the translated file for each translation locale', () => { + const diagnostics = new Diagnostics(); + const handler = new AssetTranslationHandler(fs); + const translations = [ + {locale: 'en', translations: {}}, + {locale: 'fr', translations: {}}, + ]; + const contents = Buffer.from('contents'); + handler.translate( + diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations); - expect(FileUtils.writeFile) - .toHaveBeenCalledWith('/translations/en-US/relative/path', contents); + expect(fs.readFileBuffer(enTranslationPath)).toEqual(contents); + expect(fs.readFileBuffer(frTranslationPath)).toEqual(contents); + }); + + it('should write the translated file to the source locale if provided', () => { + const diagnostics = new Diagnostics(); + const handler = new AssetTranslationHandler(fs); + const translations: TranslationBundle[] = []; + const contents = Buffer.from('contents'); + const sourceLocale = 'en-US'; + handler.translate( + diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations, + sourceLocale); + + expect(fs.readFileBuffer(enUSTranslationPath)).toEqual(contents); + }); }); }); -}); -function mockOutputPathFn(locale: string, relativePath: string) { - return `/translations/${locale}/${relativePath}`; -} + function mockOutputPathFn(locale: string, relativePath: string) { + return `/translations/${locale}/${relativePath}`; + } +}); diff --git a/packages/localize/src/tools/test/translate/integration/BUILD.bazel b/packages/localize/src/tools/test/translate/integration/BUILD.bazel index d114c63fff..eb8fca76a1 100644 --- a/packages/localize/src/tools/test/translate/integration/BUILD.bazel +++ b/packages/localize/src/tools/test/translate/integration/BUILD.bazel @@ -8,6 +8,9 @@ ts_library( ), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", + "//packages/compiler-cli/test/helpers", "//packages/localize/src/tools", ], ) diff --git a/packages/localize/src/tools/test/translate/integration/main_spec.ts b/packages/localize/src/tools/test/translate/integration/main_spec.ts index 94a9a30a11..4b2442d8d8 100644 --- a/packages/localize/src/tools/test/translate/integration/main_spec.ts +++ b/packages/localize/src/tools/test/translate/integration/main_spec.ts @@ -5,208 +5,216 @@ * 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 {resolve} from 'path'; +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 {loadTestDirectory} from '@angular/compiler-cli/test/helpers'; +import {resolve as realResolve} from 'path'; import {Diagnostics} from '../../../src/diagnostics'; -import {FileUtils} from '../../../src/file_utils'; import {translateFiles} from '../../../src/translate/main'; import {getOutputPathFn} from '../../../src/translate/output_path'; -describe('translateFiles()', () => { - const tmpDir = process.env.TEST_TMPDIR; - if (tmpDir === undefined) return; +runInEachFileSystem(() => { + describe('translateFiles()', () => { + let fs: FileSystem; + let testDir: AbsoluteFsPath; + let testFilesDir: AbsoluteFsPath; + let translationFilesDir: AbsoluteFsPath; - const testDir = resolve(tmpDir, 'translatedFiles_tests'); + beforeEach(() => { + fs = getFileSystem(); + testDir = absoluteFrom('/test'); - beforeEach(() => FileUtils.ensureDir(testDir)); - afterEach(() => { - FileUtils.remove(testDir); - }); - - it('should copy non-code files to the destination folders', () => { - const diagnostics = new Diagnostics(); - const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}')); - translateFiles({ - sourceRootPath: resolve(__dirname, 'test_files'), - sourceFilePaths: resolveAll(__dirname + '/test_files', ['test-1.txt', 'test-2.txt']), - outputPathFn, - translationFilePaths: resolveAll( - __dirname + '/locales', - ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), - translationFileLocales: [], - diagnostics, - missingTranslation: 'error', - duplicateTranslation: 'error', + testFilesDir = fs.resolve(testDir, 'test_files'); + loadTestDirectory(fs, realResolve(__dirname, 'test_files'), testFilesDir); + translationFilesDir = fs.resolve(testDir, 'test_files'); + loadTestDirectory(fs, realResolve(__dirname, 'locales'), translationFilesDir); }); - expect(diagnostics.messages.length).toEqual(0); + it('should copy non-code files to the destination folders', () => { + const diagnostics = new Diagnostics(); + const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}')); + translateFiles({ + sourceRootPath: testFilesDir, + sourceFilePaths: ['test-1.txt', 'test-2.txt'], + outputPathFn, + translationFilePaths: resolveAll( + translationFilesDir, + ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), + translationFileLocales: [], + diagnostics, + missingTranslation: 'error', + duplicateTranslation: 'error', + }); - expect(FileUtils.readFile(resolve(testDir, 'fr', 'test-1.txt'))) - .toEqual('Contents of test-1.txt'); - expect(FileUtils.readFile(resolve(testDir, 'fr', 'test-2.txt'))) - .toEqual('Contents of test-2.txt'); - expect(FileUtils.readFile(resolve(testDir, 'de', 'test-1.txt'))) - .toEqual('Contents of test-1.txt'); - expect(FileUtils.readFile(resolve(testDir, 'de', 'test-2.txt'))) - .toEqual('Contents of test-2.txt'); - expect(FileUtils.readFile(resolve(testDir, 'es', 'test-1.txt'))) - .toEqual('Contents of test-1.txt'); - expect(FileUtils.readFile(resolve(testDir, 'es', 'test-2.txt'))) - .toEqual('Contents of test-2.txt'); - expect(FileUtils.readFile(resolve(testDir, 'it', 'test-1.txt'))) - .toEqual('Contents of test-1.txt'); - expect(FileUtils.readFile(resolve(testDir, 'it', 'test-2.txt'))) - .toEqual('Contents of test-2.txt'); - }); + expect(diagnostics.messages.length).toEqual(0); - it('should translate and copy source-code files to the destination folders', () => { - const diagnostics = new Diagnostics(); - const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}')); - translateFiles({ - sourceRootPath: resolve(__dirname, 'test_files'), - sourceFilePaths: resolveAll(__dirname + '/test_files', ['test.js']), - outputPathFn, - translationFilePaths: resolveAll( - __dirname + '/locales', - ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), - translationFileLocales: [], - diagnostics, - missingTranslation: 'error', - duplicateTranslation: 'error', + expect(fs.readFile(fs.resolve(testDir, 'fr', 'test-1.txt'))) + .toEqual('Contents of test-1.txt'); + expect(fs.readFile(fs.resolve(testDir, 'fr', 'test-2.txt'))) + .toEqual('Contents of test-2.txt'); + expect(fs.readFile(fs.resolve(testDir, 'de', 'test-1.txt'))) + .toEqual('Contents of test-1.txt'); + expect(fs.readFile(fs.resolve(testDir, 'de', 'test-2.txt'))) + .toEqual('Contents of test-2.txt'); + expect(fs.readFile(fs.resolve(testDir, 'es', 'test-1.txt'))) + .toEqual('Contents of test-1.txt'); + expect(fs.readFile(fs.resolve(testDir, 'es', 'test-2.txt'))) + .toEqual('Contents of test-2.txt'); + expect(fs.readFile(fs.resolve(testDir, 'it', 'test-1.txt'))) + .toEqual('Contents of test-1.txt'); + expect(fs.readFile(fs.resolve(testDir, 'it', 'test-2.txt'))) + .toEqual('Contents of test-2.txt'); }); - expect(diagnostics.messages.length).toEqual(0); + it('should translate and copy source-code files to the destination folders', () => { + const diagnostics = new Diagnostics(); + const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}')); + translateFiles({ + sourceRootPath: testFilesDir, + sourceFilePaths: ['test.js'], + outputPathFn, + translationFilePaths: resolveAll( + translationFilesDir, + ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), + translationFileLocales: [], + diagnostics, + missingTranslation: 'error', + duplicateTranslation: 'error', + }); - expect(FileUtils.readFile(resolve(testDir, 'fr', 'test.js'))) - .toEqual(`var name="World";var message="Bonjour, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'de', 'test.js'))) - .toEqual(`var name="World";var message="Guten Tag, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'es', 'test.js'))) - .toEqual(`var name="World";var message="Hola, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'it', 'test.js'))) - .toEqual(`var name="World";var message="Ciao, "+name+"!";`); - }); + expect(diagnostics.messages.length).toEqual(0); - it('should translate and copy source-code files overriding the locales', () => { - const diagnostics = new Diagnostics(); - const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}')); - translateFiles({ - sourceRootPath: resolve(__dirname, 'test_files'), - sourceFilePaths: resolveAll(__dirname + '/test_files', ['test.js']), - outputPathFn, - translationFilePaths: resolveAll( - __dirname + '/locales', - ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), - translationFileLocales: ['xde', undefined, 'fr'], - diagnostics, - missingTranslation: 'error', - duplicateTranslation: 'error', + expect(fs.readFile(fs.resolve(testDir, 'fr', 'test.js'))) + .toEqual(`var name="World";var message="Bonjour, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'de', 'test.js'))) + .toEqual(`var name="World";var message="Guten Tag, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'es', 'test.js'))) + .toEqual(`var name="World";var message="Hola, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'it', 'test.js'))) + .toEqual(`var name="World";var message="Ciao, "+name+"!";`); }); - expect(diagnostics.messages.length).toEqual(1); - expect(diagnostics.messages).toContain({ - type: 'warning', - message: - `The provided locale "xde" does not match the target locale "de" found in the translation file "${ - resolve(__dirname, 'locales', 'messages.de.json')}".` + it('should translate and copy source-code files overriding the locales', () => { + const diagnostics = new Diagnostics(); + const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}')); + translateFiles({ + sourceRootPath: testFilesDir, + sourceFilePaths: ['test.js'], + outputPathFn, + translationFilePaths: resolveAll( + translationFilesDir, + ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), + translationFileLocales: ['xde', undefined, 'fr'], + diagnostics, + missingTranslation: 'error', + duplicateTranslation: 'error', + }); + + expect(diagnostics.messages.length).toEqual(1); + expect(diagnostics.messages).toContain({ + type: 'warning', + message: + `The provided locale "xde" does not match the target locale "de" found in the translation file "${ + fs.resolve(translationFilesDir, 'messages.de.json')}".` + }); + + expect(fs.readFile(fs.resolve(testDir, 'xde', 'test.js'))) + .toEqual(`var name="World";var message="Guten Tag, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'es', 'test.js'))) + .toEqual(`var name="World";var message="Hola, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'fr', 'test.js'))) + .toEqual(`var name="World";var message="Bonjour, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'it', 'test.js'))) + .toEqual(`var name="World";var message="Ciao, "+name+"!";`); }); - expect(FileUtils.readFile(resolve(testDir, 'xde', 'test.js'))) - .toEqual(`var name="World";var message="Guten Tag, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'es', 'test.js'))) - .toEqual(`var name="World";var message="Hola, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'fr', 'test.js'))) - .toEqual(`var name="World";var message="Bonjour, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'it', 'test.js'))) - .toEqual(`var name="World";var message="Ciao, "+name+"!";`); - }); + it('should merge translation files, if more than one provided, and translate source-code', () => { + const diagnostics = new Diagnostics(); + const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}')); + translateFiles({ + sourceRootPath: testFilesDir, + sourceFilePaths: ['test-extra.js'], + outputPathFn, + translationFilePaths: resolveAllRecursive( + translationFilesDir, + [['messages.de.json', 'messages-extra.de.json'], 'messages.es.xlf']), + translationFileLocales: [], + diagnostics, + missingTranslation: 'error', + duplicateTranslation: 'error', + }); - it('should merge translation files, if more than one provided, and translate source-code', () => { - const diagnostics = new Diagnostics(); - const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}')); - translateFiles({ - sourceRootPath: resolve(__dirname, 'test_files'), - sourceFilePaths: resolveAll(__dirname + '/test_files', ['test-extra.js']), - outputPathFn, - translationFilePaths: resolveAllRecursive( - __dirname + '/locales', - [['messages.de.json', 'messages-extra.de.json'], 'messages.es.xlf']), - translationFileLocales: [], - diagnostics, - missingTranslation: 'error', - duplicateTranslation: 'error', + expect(diagnostics.messages.length).toEqual(1); + // There is no "extra" translation in the `es` locale translation file. + expect(diagnostics.messages[0]).toEqual({ + type: 'error', + message: 'No translation found for "customExtra" ("Goodbye, {$PH}!").' + }); + + // The `de` locale translates the `customExtra` message because it is in the + // `messages-extra.de.json` file that was merged. + expect(fs.readFile(fs.resolve(testDir, 'de', 'test-extra.js'))) + .toEqual( + `var name="World";var message="Guten Tag, "+name+"!";var message="Auf wiedersehen, "+name+"!";`); + // The `es` locale does not translate `customExtra` because there is no translation for it. + expect(fs.readFile(fs.resolve(testDir, 'es', 'test-extra.js'))) + .toEqual( + `var name="World";var message="Hola, "+name+"!";var message="Goodbye, "+name+"!";`); }); - expect(diagnostics.messages.length).toEqual(1); - // There is no "extra" translation in the `es` locale translation file. - expect(diagnostics.messages[0]).toEqual({ - type: 'error', - message: 'No translation found for "customExtra" ("Goodbye, {$PH}!").' + it('should transform and/or copy files to the destination folders', () => { + const diagnostics = new Diagnostics(); + const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}')); + translateFiles({ + sourceRootPath: testFilesDir, + sourceFilePaths: ['test-1.txt', 'test-2.txt', 'test.js'], + outputPathFn, + translationFilePaths: resolveAll( + translationFilesDir, + ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), + translationFileLocales: [], + diagnostics, + missingTranslation: 'error', + duplicateTranslation: 'error', + }); + + expect(diagnostics.messages.length).toEqual(0); + + expect(fs.readFile(fs.resolve(testDir, 'fr', 'test-1.txt'))) + .toEqual('Contents of test-1.txt'); + expect(fs.readFile(fs.resolve(testDir, 'fr', 'test-2.txt'))) + .toEqual('Contents of test-2.txt'); + expect(fs.readFile(fs.resolve(testDir, 'de', 'test-1.txt'))) + .toEqual('Contents of test-1.txt'); + expect(fs.readFile(fs.resolve(testDir, 'de', 'test-2.txt'))) + .toEqual('Contents of test-2.txt'); + expect(fs.readFile(fs.resolve(testDir, 'es', 'test-1.txt'))) + .toEqual('Contents of test-1.txt'); + expect(fs.readFile(fs.resolve(testDir, 'es', 'test-2.txt'))) + .toEqual('Contents of test-2.txt'); + expect(fs.readFile(fs.resolve(testDir, 'it', 'test-1.txt'))) + .toEqual('Contents of test-1.txt'); + expect(fs.readFile(fs.resolve(testDir, 'it', 'test-2.txt'))) + .toEqual('Contents of test-2.txt'); + + expect(fs.readFile(fs.resolve(testDir, 'fr', 'test.js'))) + .toEqual(`var name="World";var message="Bonjour, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'de', 'test.js'))) + .toEqual(`var name="World";var message="Guten Tag, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'es', 'test.js'))) + .toEqual(`var name="World";var message="Hola, "+name+"!";`); + expect(fs.readFile(fs.resolve(testDir, 'it', 'test.js'))) + .toEqual(`var name="World";var message="Ciao, "+name+"!";`); }); - // The `de` locale translates the `customExtra` message because it is in the - // `messages-extra.de.json` file that was merged. - expect(FileUtils.readFile(resolve(testDir, 'de', 'test-extra.js'))) - .toEqual( - `var name="World";var message="Guten Tag, "+name+"!";var message="Auf wiedersehen, "+name+"!";`); - // The `es` locale does not translate `customExtra` because there is no translation for it. - expect(FileUtils.readFile(resolve(testDir, 'es', 'test-extra.js'))) - .toEqual( - `var name="World";var message="Hola, "+name+"!";var message="Goodbye, "+name+"!";`); - }); - - it('should transform and/or copy files to the destination folders', () => { - const diagnostics = new Diagnostics(); - const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}')); - translateFiles({ - sourceRootPath: resolve(__dirname, 'test_files'), - sourceFilePaths: - resolveAll(__dirname + '/test_files', ['test-1.txt', 'test-2.txt', 'test.js']), - outputPathFn, - translationFilePaths: resolveAll( - __dirname + '/locales', - ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']), - translationFileLocales: [], - diagnostics, - missingTranslation: 'error', - duplicateTranslation: 'error', - }); - - expect(diagnostics.messages.length).toEqual(0); - - expect(FileUtils.readFile(resolve(testDir, 'fr', 'test-1.txt'))) - .toEqual('Contents of test-1.txt'); - expect(FileUtils.readFile(resolve(testDir, 'fr', 'test-2.txt'))) - .toEqual('Contents of test-2.txt'); - expect(FileUtils.readFile(resolve(testDir, 'de', 'test-1.txt'))) - .toEqual('Contents of test-1.txt'); - expect(FileUtils.readFile(resolve(testDir, 'de', 'test-2.txt'))) - .toEqual('Contents of test-2.txt'); - expect(FileUtils.readFile(resolve(testDir, 'es', 'test-1.txt'))) - .toEqual('Contents of test-1.txt'); - expect(FileUtils.readFile(resolve(testDir, 'es', 'test-2.txt'))) - .toEqual('Contents of test-2.txt'); - expect(FileUtils.readFile(resolve(testDir, 'it', 'test-1.txt'))) - .toEqual('Contents of test-1.txt'); - expect(FileUtils.readFile(resolve(testDir, 'it', 'test-2.txt'))) - .toEqual('Contents of test-2.txt'); - - expect(FileUtils.readFile(resolve(testDir, 'fr', 'test.js'))) - .toEqual(`var name="World";var message="Bonjour, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'de', 'test.js'))) - .toEqual(`var name="World";var message="Guten Tag, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'es', 'test.js'))) - .toEqual(`var name="World";var message="Hola, "+name+"!";`); - expect(FileUtils.readFile(resolve(testDir, 'it', 'test.js'))) - .toEqual(`var name="World";var message="Ciao, "+name+"!";`); + function resolveAll(rootPath: string, paths: string[]): string[] { + return paths.map(p => fs.resolve(rootPath, p)); + } + function resolveAllRecursive( + rootPath: string, paths: (string|string[])[]): (string|string[])[] { + return paths.map( + p => Array.isArray(p) ? p.map(p2 => fs.resolve(rootPath, p2)) : fs.resolve(rootPath, p)); + } }); }); - -function resolveAll(rootPath: string, paths: string[]): string[] { - return paths.map(p => resolve(rootPath, p)); -} - -function resolveAllRecursive(rootPath: string, paths: (string|string[])[]): (string|string[])[] { - return paths.map( - p => Array.isArray(p) ? p.map(p2 => resolve(rootPath, p2)) : resolve(rootPath, p)); -} diff --git a/packages/localize/src/tools/test/translate/output_path_spec.ts b/packages/localize/src/tools/test/translate/output_path_spec.ts index f952d54706..185ac93d65 100644 --- a/packages/localize/src/tools/test/translate/output_path_spec.ts +++ b/packages/localize/src/tools/test/translate/output_path_spec.ts @@ -5,40 +5,38 @@ * 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 {getOutputPathFn} from '../../src/translate/output_path'; -describe('getOutputPathFn()', () => { - it('should return a function that joins the `outputPath` and the `relativePath`', () => { - const fn = getOutputPathFn('/output/path'); - expect(fn('en', 'relative/path')).toEqual('/output/path/relative/path'); - expect(fn('en', '../parent/path')).toEqual('/output/parent/path'); +runInEachFileSystem(() => { + describe('getOutputPathFn()', () => { + it('should return a function that joins the `outputPath` and the `relativePath`', () => { + const fn = getOutputPathFn(absoluteFrom('/output/path')); + expect(fn('en', 'relative/path')).toEqual(absoluteFrom('/output/path/relative/path')); + expect(fn('en', '../parent/path')).toEqual(absoluteFrom('/output/parent/path')); + }); + + it('should return a function that interpolates the `{{LOCALE}}` in the middle of the `outputPath`', + () => { + const fn = getOutputPathFn(absoluteFrom('/output/{{LOCALE}}/path')); + expect(fn('en', 'relative/path')).toEqual(absoluteFrom('/output/en/path/relative/path')); + expect(fn('fr', 'relative/path')).toEqual(absoluteFrom('/output/fr/path/relative/path')); + }); + + it('should return a function that interpolates the `{{LOCALE}}` in the middle of a path segment in the `outputPath`', + () => { + const fn = getOutputPathFn(absoluteFrom('/output-{{LOCALE}}-path')); + expect(fn('en', 'relative/path')).toEqual(absoluteFrom('/output-en-path/relative/path')); + expect(fn('fr', 'relative/path')).toEqual(absoluteFrom('/output-fr-path/relative/path')); + }); + + it('should return a function that interpolates the `{{LOCALE}}` at the end of the `outputPath`', + () => { + const fn = getOutputPathFn(absoluteFrom('/output/{{LOCALE}}')); + expect(fn('en', 'relative/path')).toEqual(absoluteFrom('/output/en/relative/path')); + expect(fn('fr', 'relative/path')).toEqual(absoluteFrom('/output/fr/relative/path')); + }); }); - - it('should return a function that interpolates the `{{LOCALE}}` in the middle of the `outputPath`', - () => { - const fn = getOutputPathFn('/output/{{LOCALE}}/path'); - expect(fn('en', 'relative/path')).toEqual('/output/en/path/relative/path'); - expect(fn('fr', 'relative/path')).toEqual('/output/fr/path/relative/path'); - }); - - it('should return a function that interpolates the `{{LOCALE}}` in the middle of a path segment in the `outputPath`', - () => { - const fn = getOutputPathFn('/output-{{LOCALE}}-path'); - expect(fn('en', 'relative/path')).toEqual('/output-en-path/relative/path'); - expect(fn('fr', 'relative/path')).toEqual('/output-fr-path/relative/path'); - }); - - it('should return a function that interpolates the `{{LOCALE}}` at the start of the `outputPath`', - () => { - const fn = getOutputPathFn('{{LOCALE}}/path'); - expect(fn('en', 'relative/path')).toEqual('en/path/relative/path'); - expect(fn('fr', 'relative/path')).toEqual('fr/path/relative/path'); - }); - - it('should return a function that interpolates the `{{LOCALE}}` at the end of the `outputPath`', - () => { - const fn = getOutputPathFn('/output/{{LOCALE}}'); - expect(fn('en', 'relative/path')).toEqual('/output/en/relative/path'); - expect(fn('fr', 'relative/path')).toEqual('/output/fr/relative/path'); - }); -}); \ No newline at end of file +}); diff --git a/packages/localize/src/tools/test/translate/source_files/source_file_translation_handler_spec.ts b/packages/localize/src/tools/test/translate/source_files/source_file_translation_handler_spec.ts index ef56567330..bef3127419 100644 --- a/packages/localize/src/tools/test/translate/source_files/source_file_translation_handler_spec.ts +++ b/packages/localize/src/tools/test/translate/source_files/source_file_translation_handler_spec.ts @@ -5,128 +5,135 @@ * 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, PathSegment, relativeFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; + import {Diagnostics} from '../../../src/diagnostics'; -import {FileUtils} from '../../../src/file_utils'; import {SourceFileTranslationHandler} from '../../../src/translate/source_files/source_file_translation_handler'; import {TranslationBundle} from '../../../src/translate/translator'; -describe('SourceFileTranslationHandler', () => { - describe('canTranslate()', () => { - it('should return true if the path ends in ".js"', () => { - const handler = new SourceFileTranslationHandler(); - expect(handler.canTranslate('relative/path', Buffer.from('contents'))).toBe(false); - expect(handler.canTranslate('relative/path.js', Buffer.from('contents'))).toBe(true); - }); - }); +runInEachFileSystem(() => { + describe('SourceFileTranslationHandler', () => { + let fs: FileSystem; + let rootPath: AbsoluteFsPath; + let filePath: PathSegment; + let enTranslationPath: AbsoluteFsPath; + let enUSTranslationPath: AbsoluteFsPath; + let frTranslationPath: AbsoluteFsPath; - describe('translate()', () => { beforeEach(() => { - spyOn(FileUtils, 'writeFile'); + fs = getFileSystem(); + rootPath = absoluteFrom('/root/path'); + filePath = relativeFrom('relative/path.js'); + enTranslationPath = absoluteFrom('/translations/en/relative/path.js'); + enUSTranslationPath = absoluteFrom('/translations/en-US/relative/path.js'); + frTranslationPath = absoluteFrom('/translations/fr/relative/path.js'); }); - it('should copy files for each translation locale if they contain no reference to `$localize`', - () => { - const diagnostics = new Diagnostics(); - const handler = new SourceFileTranslationHandler(); - const translations = [ - {locale: 'en', translations: {}}, - {locale: 'fr', translations: {}}, - ]; - const contents = Buffer.from('contents'); - handler.translate( - diagnostics, '/root/path', 'relative/path', contents, mockOutputPathFn, translations); - - expect(FileUtils.writeFile) - .toHaveBeenCalledWith('/translations/en/relative/path', contents); - expect(FileUtils.writeFile) - .toHaveBeenCalledWith('/translations/fr/relative/path', contents); - }); - - it('should copy files to the source locale if they contain no reference to `$localize` and `sourceLocale` is provided', - () => { - const diagnostics = new Diagnostics(); - const handler = new SourceFileTranslationHandler(); - const translations: TranslationBundle[] = []; - const contents = Buffer.from('contents'); - handler.translate( - diagnostics, '/root/path', 'relative/path', contents, mockOutputPathFn, translations, - 'en-US'); - expect(FileUtils.writeFile) - .toHaveBeenCalledWith('/translations/en-US/relative/path', contents); - }); - - it('should transform each $localize template tag', () => { - const diagnostics = new Diagnostics(); - const handler = new SourceFileTranslationHandler(); - const translations = [ - {locale: 'en', translations: {}}, - {locale: 'fr', translations: {}}, - ]; - const contents = Buffer.from( - '$localize`a${1}b${2}c`;\n' + - '$localize(__makeTemplateObject(["a", "b", "c"], ["a", "b", "c"]), 1, 2);'); - const output = '"a"+1+"b"+2+"c";"a"+1+"b"+2+"c";'; - handler.translate( - diagnostics, '/root/path', 'relative/path.js', contents, mockOutputPathFn, translations); - - expect(FileUtils.writeFile).toHaveBeenCalledWith('/translations/en/relative/path.js', output); - expect(FileUtils.writeFile).toHaveBeenCalledWith('/translations/fr/relative/path.js', output); + describe('canTranslate()', () => { + it('should return true if the path ends in ".js"', () => { + const handler = new SourceFileTranslationHandler(fs); + expect(handler.canTranslate(relativeFrom('relative/path'), Buffer.from('contents'))) + .toBe(false); + expect(handler.canTranslate(filePath, Buffer.from('contents'))).toBe(true); + }); }); - it('should transform each $localize template tag and write it to the source locale if provided', - () => { - const diagnostics = new Diagnostics(); - const handler = new SourceFileTranslationHandler(); - const translations: TranslationBundle[] = []; - const contents = Buffer.from( - '$localize`a${1}b${2}c`;\n' + - '$localize(__makeTemplateObject(["a", "b", "c"], ["a", "b", "c"]), 1, 2);'); - const output = '"a"+1+"b"+2+"c";"a"+1+"b"+2+"c";'; - handler.translate( - diagnostics, '/root/path', 'relative/path.js', contents, mockOutputPathFn, - translations, 'en-US'); + describe('translate()', () => { + it('should copy files for each translation locale if they contain no reference to `$localize`', + () => { + const diagnostics = new Diagnostics(); + const handler = new SourceFileTranslationHandler(fs); + const translations = [ + {locale: 'en', translations: {}}, + {locale: 'fr', translations: {}}, + ]; + const contents = Buffer.from('contents'); + handler.translate( + diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations); - expect(FileUtils.writeFile) - .toHaveBeenCalledWith('/translations/en-US/relative/path.js', output); - }); + expect(fs.readFileBuffer(enTranslationPath)).toEqual(contents); + expect(fs.readFileBuffer(frTranslationPath)).toEqual(contents); + }); - it('should transform `$localize.locale` identifiers', () => { - const diagnostics = new Diagnostics(); - const handler = new SourceFileTranslationHandler(); - const translations: TranslationBundle[] = [ - {locale: 'fr', translations: {}}, - ]; - const contents = Buffer.from( - 'const x = $localize.locale;\n' + - 'const y = typeof $localize !== "undefined" && $localize.locale;\n' + - 'const z = "undefined" !== typeof $localize && $localize.locale || "default";'); - const getOutput = (locale: string) => - `const x="${locale}";const y="${locale}";const z="${locale}"||"default";`; + it('should copy files to the source locale if they contain no reference to `$localize` and `sourceLocale` is provided', + () => { + const diagnostics = new Diagnostics(); + const handler = new SourceFileTranslationHandler(fs); + const translations: TranslationBundle[] = []; + const contents = Buffer.from('contents'); + handler.translate( + diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations, 'en-US'); + expect(fs.readFileBuffer(enUSTranslationPath)).toEqual(contents); + }); - handler.translate( - diagnostics, '/root/path', 'relative/path.js', contents, mockOutputPathFn, translations, - 'en-US'); + it('should transform each $localize template tag', () => { + const diagnostics = new Diagnostics(); + const handler = new SourceFileTranslationHandler(fs); + const translations = [ + {locale: 'en', translations: {}}, + {locale: 'fr', translations: {}}, + ]; + const contents = Buffer.from( + '$localize`a${1}b${2}c`;\n' + + '$localize(__makeTemplateObject(["a", "b", "c"], ["a", "b", "c"]), 1, 2);'); + const output = '"a"+1+"b"+2+"c";"a"+1+"b"+2+"c";'; + handler.translate( + diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations); - expect(FileUtils.writeFile) - .toHaveBeenCalledWith('/translations/fr/relative/path.js', getOutput('fr')); - expect(FileUtils.writeFile) - .toHaveBeenCalledWith('/translations/en-US/relative/path.js', getOutput('en-US')); - }); + expect(fs.readFile(enTranslationPath)).toEqual(output); + expect(fs.readFile(frTranslationPath)).toEqual(output); + }); - it('should error if the file is not valid JS', () => { - const diagnostics = new Diagnostics(); - const handler = new SourceFileTranslationHandler(); - const translations = [{locale: 'en', translations: {}}]; - const contents = Buffer.from('this is not a valid $localize file.'); - expect( - () => handler.translate( - diagnostics, '/root/path', 'relative/path.js', contents, mockOutputPathFn, - translations)) - .toThrowError(); + it('should transform each $localize template tag and write it to the source locale if provided', + () => { + const diagnostics = new Diagnostics(); + const handler = new SourceFileTranslationHandler(fs); + const translations: TranslationBundle[] = []; + const contents = Buffer.from( + '$localize`a${1}b${2}c`;\n' + + '$localize(__makeTemplateObject(["a", "b", "c"], ["a", "b", "c"]), 1, 2);'); + const output = '"a"+1+"b"+2+"c";"a"+1+"b"+2+"c";'; + handler.translate( + diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations, 'en-US'); + + expect(fs.readFile(enUSTranslationPath)).toEqual(output); + }); + + it('should transform `$localize.locale` identifiers', () => { + const diagnostics = new Diagnostics(); + const handler = new SourceFileTranslationHandler(fs); + const translations: TranslationBundle[] = [ + {locale: 'fr', translations: {}}, + ]; + const contents = Buffer.from( + 'const x = $localize.locale;\n' + + 'const y = typeof $localize !== "undefined" && $localize.locale;\n' + + 'const z = "undefined" !== typeof $localize && $localize.locale || "default";'); + const getOutput = (locale: string) => + `const x="${locale}";const y="${locale}";const z="${locale}"||"default";`; + + handler.translate( + diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations, 'en-US'); + + expect(fs.readFile(frTranslationPath)).toEqual(getOutput('fr')); + expect(fs.readFile(enUSTranslationPath)).toEqual(getOutput('en-US')); + }); + + it('should error if the file is not valid JS', () => { + const diagnostics = new Diagnostics(); + const handler = new SourceFileTranslationHandler(fs); + const translations = [{locale: 'en', translations: {}}]; + const contents = Buffer.from('this is not a valid $localize file.'); + expect( + () => handler.translate( + diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations)) + .toThrowError(); + }); }); }); -}); -function mockOutputPathFn(locale: string, relativePath: string) { - return `/translations/${locale}/${relativePath}`; -} + function mockOutputPathFn(locale: string, relativePath: string) { + return `/translations/${locale}/${relativePath}`; + } +}); diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts index 147a12185c..6c251260fc 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts @@ -5,216 +5,229 @@ * 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 {ɵParsedTranslation, ɵparseTranslation} from '@angular/localize'; import {DiagnosticHandlingStrategy, Diagnostics} from '../../../src/diagnostics'; -import {FileUtils} from '../../../src/file_utils'; import {TranslationLoader} from '../../../src/translate/translation_files/translation_loader'; +import {SimpleJsonTranslationParser} from '../../../src/translate/translation_files/translation_parsers/simple_json_translation_parser'; import {TranslationParser} from '../../../src/translate/translation_files/translation_parsers/translation_parser'; -describe('TranslationLoader', () => { - describe('loadBundles()', () => { - const alwaysCanParse = () => true; - const neverCanParse = () => false; +runInEachFileSystem(() => { + describe('TranslationLoader', () => { + describe('loadBundles()', () => { + const alwaysCanParse = () => true; + const neverCanParse = () => false; - beforeEach(() => { - spyOn(FileUtils, 'readFile').and.returnValues('english messages', 'french messages'); - }); + let fs: FileSystem; + let enTranslationPath: AbsoluteFsPath; + const enTranslationContent = '{"locale": "en", "translations": {"a": "A"}}'; + let frTranslationPath: AbsoluteFsPath; + const frTranslationContent = '{"locale": "fr", "translations": {"a": "A"}}'; + let frExtraTranslationPath: AbsoluteFsPath; + const frExtraTranslationContent = '{"locale": "fr", "translations": {"b": "B"}}'; + let jsonParser: SimpleJsonTranslationParser; - it('should call `canParse()` and `parse()` for each file', () => { - const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(alwaysCanParse, 'fr'); - const loader = new TranslationLoader([parser], 'error', diagnostics); - loader.loadBundles([['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []); - expect(parser.log).toEqual([ - 'canParse(/src/locale/messages.en.xlf, english messages)', - 'parse(/src/locale/messages.en.xlf, english messages)', - 'canParse(/src/locale/messages.fr.xlf, french messages)', - 'parse(/src/locale/messages.fr.xlf, french messages)', - ]); - }); + beforeEach(() => { + fs = getFileSystem(); + enTranslationPath = absoluteFrom('/src/locale/messages.en.json'); + frTranslationPath = absoluteFrom('/src/locale/messages.fr.json'); + frExtraTranslationPath = absoluteFrom('/src/locale/extra.fr.json'); + fs.ensureDir(absoluteFrom('/src/locale')); + fs.writeFile(enTranslationPath, enTranslationContent); + fs.writeFile(frTranslationPath, frTranslationContent); + fs.writeFile(frExtraTranslationPath, frExtraTranslationContent); + jsonParser = new SimpleJsonTranslationParser(); + }); - it('should stop at the first parser that can parse each file', () => { - const diagnostics = new Diagnostics(); - const parser1 = new MockTranslationParser(neverCanParse); - const parser2 = new MockTranslationParser(alwaysCanParse, 'fr'); - const parser3 = new MockTranslationParser(alwaysCanParse, 'en'); - const loader = new TranslationLoader([parser1, parser2, parser3], 'error', diagnostics); - loader.loadBundles([['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []); - expect(parser1.log).toEqual([ - 'canParse(/src/locale/messages.en.xlf, english messages)', - 'canParse(/src/locale/messages.fr.xlf, french messages)', - ]); - expect(parser2.log).toEqual([ - 'canParse(/src/locale/messages.en.xlf, english messages)', - 'parse(/src/locale/messages.en.xlf, english messages)', - 'canParse(/src/locale/messages.fr.xlf, french messages)', - 'parse(/src/locale/messages.fr.xlf, french messages)', - ]); - }); + it('should call `canParse()` and `parse()` for each file', () => { + const diagnostics = new Diagnostics(); + const parser = new MockTranslationParser(alwaysCanParse, 'fr'); + const loader = new TranslationLoader(fs, [parser], 'error', diagnostics); + loader.loadBundles([[enTranslationPath], [frTranslationPath]], []); + expect(parser.log).toEqual([ + `canParse(${enTranslationPath}, ${enTranslationContent})`, + `parse(${enTranslationPath}, ${enTranslationContent})`, + `canParse(${frTranslationPath}, ${frTranslationContent})`, + `parse(${frTranslationPath}, ${frTranslationContent})`, + ]); + }); - it('should return locale and translations parsed from each file', () => { - const translations = {}; - const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(alwaysCanParse, 'pl', translations); - const loader = new TranslationLoader([parser], 'error', diagnostics); - const result = loader.loadBundles( - [['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []); - expect(result).toEqual([ - {locale: 'pl', translations, diagnostics: new Diagnostics()}, - {locale: 'pl', translations, diagnostics: new Diagnostics()}, - ]); - }); + it('should stop at the first parser that can parse each file', () => { + const diagnostics = new Diagnostics(); + const parser1 = new MockTranslationParser(neverCanParse); + const parser2 = new MockTranslationParser(alwaysCanParse, 'fr'); + const parser3 = new MockTranslationParser(alwaysCanParse, 'en'); + const loader = new TranslationLoader(fs, [parser1, parser2, parser3], 'error', diagnostics); + loader.loadBundles([[enTranslationPath], [frTranslationPath]], []); + expect(parser1.log).toEqual([ + `canParse(${enTranslationPath}, ${enTranslationContent})`, + `canParse(${frTranslationPath}, ${frTranslationContent})`, + ]); + expect(parser2.log).toEqual([ + `canParse(${enTranslationPath}, ${enTranslationContent})`, + `parse(${enTranslationPath}, ${enTranslationContent})`, + `canParse(${frTranslationPath}, ${frTranslationContent})`, + `parse(${frTranslationPath}, ${frTranslationContent})`, + ]); + }); - it('should return the provided locale if there is no parsed locale', () => { - const translations = {}; - const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(alwaysCanParse, undefined, translations); - const loader = new TranslationLoader([parser], 'error', diagnostics); - const result = loader.loadBundles( - [['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], ['en', 'fr']); - expect(result).toEqual([ - {locale: 'en', translations, diagnostics: new Diagnostics()}, - {locale: 'fr', translations, diagnostics: new Diagnostics()}, - ]); - }); + it('should return locale and translations parsed from each file', () => { + const translations = {}; + const diagnostics = new Diagnostics(); + const parser = new MockTranslationParser(alwaysCanParse, 'pl', translations); + const loader = new TranslationLoader(fs, [parser], 'error', diagnostics); + const result = loader.loadBundles([[enTranslationPath], [frTranslationPath]], []); + expect(result).toEqual([ + {locale: 'pl', translations, diagnostics: new Diagnostics()}, + {locale: 'pl', translations, diagnostics: new Diagnostics()}, + ]); + }); - it('should merge multiple translation files, if given, for a each locale', () => { - const diagnostics = new Diagnostics(); - const parser1 = new MockTranslationParser( - f => f.includes('messages.fr'), 'fr', {'a': ɵparseTranslation('A')}); - const parser2 = new MockTranslationParser( - f => f.includes('extra.fr'), 'fr', {'b': ɵparseTranslation('B')}); - const loader = new TranslationLoader([parser1, parser2], 'error', diagnostics); - const result = - loader.loadBundles([['/src/locale/messages.fr.xlf', '/src/locale/extra.fr.xlf']], []); - expect(result).toEqual([ - { - locale: 'fr', - translations: {'a': ɵparseTranslation('A'), 'b': ɵparseTranslation('B')}, - diagnostics: new Diagnostics(), - }, - ]); - }); + it('should return the provided locale if there is no parsed locale', () => { + const translations = {}; + const diagnostics = new Diagnostics(); + const parser = new MockTranslationParser(alwaysCanParse, undefined, translations); + const loader = new TranslationLoader(fs, [parser], 'error', diagnostics); + const result = loader.loadBundles([[enTranslationPath], [frTranslationPath]], ['en', 'fr']); + expect(result).toEqual([ + {locale: 'en', translations, diagnostics: new Diagnostics()}, + {locale: 'fr', translations, diagnostics: new Diagnostics()}, + ]); + }); - const allDiagnosticModes: DiagnosticHandlingStrategy[] = ['ignore', 'warning', 'error']; - allDiagnosticModes.forEach( - mode => it( - `should ${mode} on duplicate messages when merging multiple translation files`, () => { - const diagnostics = new Diagnostics(); - const parser1 = new MockTranslationParser( - f => f.includes('messages.fr'), 'fr', {'a': ɵparseTranslation('A')}); - const parser2 = new MockTranslationParser( - f => f.includes('extra.fr'), 'fr', {'a': ɵparseTranslation('B')}); - const loader = new TranslationLoader([parser1, parser2], mode, diagnostics); - const result = loader.loadBundles( - [['/src/locale/messages.fr.xlf', '/src/locale/extra.fr.xlf']], []); - expect(result).toEqual([ - { - locale: 'fr', - translations: {'a': ɵparseTranslation('A')}, - diagnostics: jasmine.any(Diagnostics), - }, - ]); - - if (mode === 'error' || mode === 'warning') { - expect(diagnostics.messages).toEqual([{ - type: mode, - message: - `Duplicate translations for message "a" when merging "/src/locale/extra.fr.xlf".` - }]); - } - })); - - it('should warn if the provided locales do not match the parsed locales', () => { - const translations = {}; - const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(alwaysCanParse, 'pl', translations); - const loader = new TranslationLoader([parser], 'error', diagnostics); - loader.loadBundles( - [['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], [undefined, 'FR']); - expect(diagnostics.messages.length).toEqual(1); - expect(diagnostics.messages) - .toContain( - { - type: 'warning', - message: - `The provided locale "FR" does not match the target locale "pl" found in the translation file "/src/locale/messages.fr.xlf".`, - }, - ); - }); - - it('should warn on differing target locales when merging multiple translation files', () => { - const diagnostics = new Diagnostics(); - const parser1 = new MockTranslationParser( - f => f.includes('messages-1.fr'), 'fr', {'a': ɵparseTranslation('A')}); - const parser2 = new MockTranslationParser( - f => f.includes('messages-2.fr'), 'fr', {'b': ɵparseTranslation('B')}); - const parser3 = new MockTranslationParser( - f => f.includes('messages.de'), 'de', {'c': ɵparseTranslation('C')}); - const loader = new TranslationLoader([parser1, parser2, parser3], 'error', diagnostics); - const result = loader.loadBundles( - [[ - '/src/locale/messages-1.fr.xlf', '/src/locale/messages-2.fr.xlf', - '/src/locale/messages.de.xlf' - ]], - []); - expect(result).toEqual([ - { - locale: 'fr', - translations: { - 'a': ɵparseTranslation('A'), - 'b': ɵparseTranslation('B'), - 'c': ɵparseTranslation('C') + it('should merge multiple translation files, if given, for a each locale', () => { + const diagnostics = new Diagnostics(); + const loader = new TranslationLoader(fs, [jsonParser], 'error', diagnostics); + const result = loader.loadBundles([[frTranslationPath, frExtraTranslationPath]], []); + expect(result).toEqual([ + { + locale: 'fr', + translations: {'a': ɵparseTranslation('A'), 'b': ɵparseTranslation('B')}, + diagnostics: new Diagnostics(), }, - diagnostics: jasmine.any(Diagnostics), - }, - ]); + ]); + }); - expect(diagnostics.messages).toEqual([{ - type: 'warning', - message: - `When merging multiple translation files, the target locale "de" found in "/src/locale/messages.de.xlf" ` + - `does not match the target locale "fr" found in earlier files ["/src/locale/messages-1.fr.xlf", "/src/locale/messages-2.fr.xlf"].` - }]); - }); + const allDiagnosticModes: DiagnosticHandlingStrategy[] = ['ignore', 'warning', 'error']; + allDiagnosticModes.forEach( + mode => + it(`should ${mode} on duplicate messages when merging multiple translation files`, + () => { + const diagnostics = new Diagnostics(); + const loader = new TranslationLoader(fs, [jsonParser], mode, diagnostics); + // Change the fs-extra file to have the same translations as fr. + fs.writeFile(frExtraTranslationPath, frTranslationContent); + const result = + loader.loadBundles([[frTranslationPath, frExtraTranslationPath]], []); + expect(result).toEqual([ + { + locale: 'fr', + translations: {'a': ɵparseTranslation('A')}, + diagnostics: jasmine.any(Diagnostics), + }, + ]); - it('should throw an error if there is no provided nor parsed target locale', () => { - const translations = {}; - const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(alwaysCanParse, undefined, translations); - const loader = new TranslationLoader([parser], 'error', diagnostics); - expect(() => loader.loadBundles([['/src/locale/messages.en.xlf']], [])) - .toThrowError( - 'The translation file "/src/locale/messages.en.xlf" does not contain a target locale and no explicit locale was provided for this file.'); - }); + if (mode === 'error' || mode === 'warning') { + expect(diagnostics.messages).toEqual([{ + type: mode, + message: `Duplicate translations for message "a" when merging "${ + frExtraTranslationPath}".` + }]); + } + })); - it('should error if none of the parsers can parse the file', () => { - const diagnostics = new Diagnostics(); - const parser = new MockTranslationParser(neverCanParse); - const loader = new TranslationLoader([parser], 'error', diagnostics); - expect( - () => loader.loadBundles( - [['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], [])) - .toThrowError( - 'There is no "TranslationParser" that can parse this translation file: /src/locale/messages.en.xlf.'); + it('should warn if the provided locales do not match the parsed locales', () => { + const diagnostics = new Diagnostics(); + const loader = new TranslationLoader(fs, [jsonParser], 'error', diagnostics); + loader.loadBundles([[enTranslationPath], [frTranslationPath]], [undefined, 'es']); + expect(diagnostics.messages.length).toEqual(1); + expect(diagnostics.messages) + .toContain( + { + type: 'warning', + message: + `The provided locale "es" does not match the target locale "fr" found in the translation file "${ + frTranslationPath}".`, + }, + ); + }); + + it('should warn on differing target locales when merging multiple translation files', () => { + const diagnostics = new Diagnostics(); + + const fr1 = absoluteFrom('/src/locale/messages-1.fr.json'); + fs.writeFile(fr1, '{"locale":"fr", "translations": {"a": "A"}}'); + + const fr2 = absoluteFrom('/src/locale/messages-2.fr.json'); + fs.writeFile(fr2, '{"locale":"fr", "translations": {"b": "B"}}'); + + const de = absoluteFrom('/src/locale/messages.de.json'); + fs.writeFile(de, '{"locale":"de", "translations": {"c": "C"}}'); + + const loader = new TranslationLoader(fs, [jsonParser], 'error', diagnostics); + + const result = loader.loadBundles([[fr1, fr2, de]], []); + expect(result).toEqual([ + { + locale: 'fr', + translations: { + 'a': ɵparseTranslation('A'), + 'b': ɵparseTranslation('B'), + 'c': ɵparseTranslation('C') + }, + diagnostics: jasmine.any(Diagnostics), + }, + ]); + + expect(diagnostics.messages).toEqual([{ + type: 'warning', + message: + `When merging multiple translation files, the target locale "de" found in "${de}" ` + + `does not match the target locale "fr" found in earlier files ["${fr1}", "${fr2}"].` + }]); + }); + + it('should throw an error if there is no provided nor parsed target locale', () => { + const translations = {}; + const diagnostics = new Diagnostics(); + const parser = new MockTranslationParser(alwaysCanParse, undefined, translations); + const loader = new TranslationLoader(fs, [parser], 'error', diagnostics); + expect(() => loader.loadBundles([[enTranslationPath]], [])) + .toThrowError(`The translation file "${ + enTranslationPath}" does not contain a target locale and no explicit locale was provided for this file.`); + }); + + it('should error if none of the parsers can parse the file', () => { + const diagnostics = new Diagnostics(); + const parser = new MockTranslationParser(neverCanParse); + const loader = new TranslationLoader(fs, [parser], 'error', diagnostics); + expect(() => loader.loadBundles([[enTranslationPath], [frTranslationPath]], [])) + .toThrowError(`There is no "TranslationParser" that can parse this translation file: ${ + enTranslationPath}.`); + }); }); }); + + class MockTranslationParser implements TranslationParser { + log: string[] = []; + constructor( + private _canParse: (filePath: string) => boolean, private _locale?: string, + private _translations: Record = {}) {} + + canParse(filePath: string, fileContents: string) { + this.log.push(`canParse(${filePath}, ${fileContents})`); + return this._canParse(filePath); + } + + parse(filePath: string, fileContents: string) { + this.log.push(`parse(${filePath}, ${fileContents})`); + return { + locale: this._locale, + translations: this._translations, + diagnostics: new Diagnostics() + }; + } + } }); - -class MockTranslationParser implements TranslationParser { - log: string[] = []; - constructor( - private _canParse: (filePath: string) => boolean, private _locale?: string, - private _translations: Record = {}) {} - - canParse(filePath: string, fileContents: string) { - this.log.push(`canParse(${filePath}, ${fileContents})`); - return this._canParse(filePath); - } - - parse(filePath: string, fileContents: string) { - this.log.push(`parse(${filePath}, ${fileContents})`); - return {locale: this._locale, translations: this._translations, diagnostics: new Diagnostics()}; - } -} \ No newline at end of file diff --git a/packages/localize/src/tools/test/translate/translator_spec.ts b/packages/localize/src/tools/test/translate/translator_spec.ts index 17acdeba76..62763ddc24 100644 --- a/packages/localize/src/tools/test/translate/translator_spec.ts +++ b/packages/localize/src/tools/test/translate/translator_spec.ts @@ -5,111 +5,125 @@ * 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, PathSegment, relativeFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; + import {Diagnostics as Diagnostics} from '../../src/diagnostics'; -import {FileUtils} from '../../src/file_utils'; import {OutputPathFn} from '../../src/translate/output_path'; import {TranslationBundle, TranslationHandler, Translator} from '../../src/translate/translator'; -describe('Translator', () => { - describe('translateFiles()', () => { +runInEachFileSystem(() => { + describe('Translator', () => { + let fs: FileSystem; + let distDirectory: AbsoluteFsPath; + let imgDirectory: AbsoluteFsPath; + let file1Path: PathSegment; + let imgPath: PathSegment; + beforeEach(() => { - spyOn(FileUtils, 'readFileBuffer') - .and.returnValues(Buffer.from('resource file 1'), Buffer.from('resource file 2')); + fs = getFileSystem(); + distDirectory = absoluteFrom('/dist'); + imgDirectory = absoluteFrom('/dist/images'); + file1Path = relativeFrom('file1.js'); + imgPath = relativeFrom('images/img.gif'); + + fs.ensureDir(imgDirectory); + fs.writeFile(fs.resolve(distDirectory, file1Path), 'resource file 1'); + fs.writeFile(fs.resolve(distDirectory, imgPath), Buffer.from('resource file 2')); }); - it('should call FileUtils.readFileBuffer to load the resource file contents', () => { - const translator = new Translator([new MockTranslationHandler()], new Diagnostics()); - translator.translateFiles( - ['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, []); - expect(FileUtils.readFileBuffer).toHaveBeenCalledWith('/dist/file1.js'); - expect(FileUtils.readFileBuffer).toHaveBeenCalledWith('/dist/images/img.gif'); - }); + describe('translateFiles()', () => { + it('should call FileSystem.readFileBuffer load the resource file contents', () => { + const translator = new Translator(fs, [new MockTranslationHandler()], new Diagnostics()); + spyOn(fs, 'readFileBuffer').and.callThrough(); + translator.translateFiles([file1Path, imgPath], distDirectory, mockOutputPathFn, []); + expect(fs.readFileBuffer).toHaveBeenCalledWith(fs.resolve(distDirectory, file1Path)); + expect(fs.readFileBuffer).toHaveBeenCalledWith(fs.resolve(distDirectory, imgPath)); + }); - it('should call `canTranslate()` and `translate()` for each file', () => { - const diagnostics = new Diagnostics(); - const handler = new MockTranslationHandler(true); - const translator = new Translator([handler], diagnostics); - translator.translateFiles( - ['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, []); + it('should call `canTranslate()` and `translate()` for each file', () => { + const diagnostics = new Diagnostics(); + const handler = new MockTranslationHandler(true); + const translator = new Translator(fs, [handler], diagnostics); + translator.translateFiles([file1Path, imgPath], distDirectory, mockOutputPathFn, []); - expect(handler.log).toEqual([ - 'canTranslate(file1.js, resource file 1)', - 'translate(/dist, file1.js, resource file 1, ...)', - 'canTranslate(images/img.gif, resource file 2)', - 'translate(/dist, images/img.gif, resource file 2, ...)', - ]); - }); + expect(handler.log).toEqual([ + 'canTranslate(file1.js, resource file 1)', + `translate(${distDirectory}, file1.js, resource file 1, ...)`, + 'canTranslate(images/img.gif, resource file 2)', + `translate(${distDirectory}, images/img.gif, resource file 2, ...)`, + ]); + }); - it('should pass the sourceLocale through to `translate()` if provided', () => { - const diagnostics = new Diagnostics(); - const handler = new MockTranslationHandler(true); - const translator = new Translator([handler], diagnostics); - translator.translateFiles( - ['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, [], 'en-US'); + it('should pass the sourceLocale through to `translate()` if provided', () => { + const diagnostics = new Diagnostics(); + const handler = new MockTranslationHandler(true); + const translator = new Translator(fs, [handler], diagnostics); + translator.translateFiles( + [file1Path, imgPath], distDirectory, mockOutputPathFn, [], 'en-US'); - expect(handler.log).toEqual([ - 'canTranslate(file1.js, resource file 1)', - 'translate(/dist, file1.js, resource file 1, ..., en-US)', - 'canTranslate(images/img.gif, resource file 2)', - 'translate(/dist, images/img.gif, resource file 2, ..., en-US)', - ]); - }); + expect(handler.log).toEqual([ + 'canTranslate(file1.js, resource file 1)', + `translate(${distDirectory}, file1.js, resource file 1, ..., en-US)`, + 'canTranslate(images/img.gif, resource file 2)', + `translate(${distDirectory}, images/img.gif, resource file 2, ..., en-US)`, + ]); + }); - it('should stop at the first handler that can handle each file', () => { - const diagnostics = new Diagnostics(); - const handler1 = new MockTranslationHandler(false); - const handler2 = new MockTranslationHandler(true); - const handler3 = new MockTranslationHandler(true); - const translator = new Translator([handler1, handler2, handler3], diagnostics); - translator.translateFiles( - ['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, []); + it('should stop at the first handler that can handle each file', () => { + const diagnostics = new Diagnostics(); + const handler1 = new MockTranslationHandler(false); + const handler2 = new MockTranslationHandler(true); + const handler3 = new MockTranslationHandler(true); + const translator = new Translator(fs, [handler1, handler2, handler3], diagnostics); + translator.translateFiles([file1Path, imgPath], distDirectory, mockOutputPathFn, []); - expect(handler1.log).toEqual([ - 'canTranslate(file1.js, resource file 1)', - 'canTranslate(images/img.gif, resource file 2)', - ]); - expect(handler2.log).toEqual([ - 'canTranslate(file1.js, resource file 1)', - 'translate(/dist, file1.js, resource file 1, ...)', - 'canTranslate(images/img.gif, resource file 2)', - 'translate(/dist, images/img.gif, resource file 2, ...)', - ]); - }); + expect(handler1.log).toEqual([ + 'canTranslate(file1.js, resource file 1)', + 'canTranslate(images/img.gif, resource file 2)', + ]); + expect(handler2.log).toEqual([ + 'canTranslate(file1.js, resource file 1)', + `translate(${distDirectory}, file1.js, resource file 1, ...)`, + 'canTranslate(images/img.gif, resource file 2)', + `translate(${distDirectory}, images/img.gif, resource file 2, ...)`, + ]); + }); - it('should error if none of the handlers can handle the file', () => { - const diagnostics = new Diagnostics(); - const handler = new MockTranslationHandler(false); - const translator = new Translator([handler], diagnostics); + it('should error if none of the handlers can handle the file', () => { + const diagnostics = new Diagnostics(); + const handler = new MockTranslationHandler(false); + const translator = new Translator(fs, [handler], diagnostics); - translator.translateFiles( - ['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, []); + translator.translateFiles([file1Path, imgPath], distDirectory, mockOutputPathFn, []); - expect(diagnostics.messages).toEqual([ - {type: 'error', message: 'Unable to handle resource file: /dist/file1.js'}, - {type: 'error', message: 'Unable to handle resource file: /dist/images/img.gif'}, - ]); + expect(diagnostics.messages).toEqual([ + {type: 'error', message: `Unable to handle resource file: ${file1Path}`}, + {type: 'error', message: `Unable to handle resource file: ${imgPath}`}, + ]); + }); }); }); + + class MockTranslationHandler implements TranslationHandler { + log: string[] = []; + constructor(private _canTranslate: boolean = true) {} + + canTranslate(relativePath: string, contents: Buffer) { + this.log.push(`canTranslate(${relativePath}, ${contents.toString('utf8')})`); + return this._canTranslate; + } + + translate( + _diagnostics: Diagnostics, rootPath: string, relativePath: string, contents: Buffer, + _outputPathFn: OutputPathFn, _translations: TranslationBundle[], sourceLocale?: string) { + this.log.push( + `translate(${rootPath}, ${relativePath}, ${contents}, ...` + + (sourceLocale !== undefined ? `, ${sourceLocale})` : ')')); + } + } + + function mockOutputPathFn(locale: string, relativePath: string) { + return `translations/${locale}/${relativePath}`; + } }); - -class MockTranslationHandler implements TranslationHandler { - log: string[] = []; - constructor(private _canTranslate: boolean = true) {} - - canTranslate(relativePath: string, contents: Buffer) { - this.log.push(`canTranslate(${relativePath}, ${contents.toString('utf8')})`); - return this._canTranslate; - } - - translate( - _diagnostics: Diagnostics, rootPath: string, relativePath: string, contents: Buffer, - _outputPathFn: OutputPathFn, _translations: TranslationBundle[], sourceLocale?: string) { - this.log.push( - `translate(${rootPath}, ${relativePath}, ${contents}, ...` + - (sourceLocale !== undefined ? `, ${sourceLocale})` : ')')); - } -} - -function mockOutputPathFn(locale: string, relativePath: string) { - return `translations/${locale}/${relativePath}`; -}