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
This commit is contained in:
parent
dbf1f6b233
commit
141fcb95a4
|
@ -28,6 +28,10 @@
|
||||||
"glob": "7.1.2",
|
"glob": "7.1.2",
|
||||||
"yargs": "15.3.0"
|
"yargs": "15.3.0"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/compiler": "0.0.0-PLACEHOLDER",
|
||||||
|
"@angular/compiler-cli": "0.0.0-PLACEHOLDER"
|
||||||
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"registry": "https://wombat-dressing-room.appspot.com"
|
"registry": "https://wombat-dressing-room.appspot.com"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ ts_library(
|
||||||
tsconfig = ":tsconfig",
|
tsconfig = ":tsconfig",
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||||
"//packages/localize",
|
"//packages/localize",
|
||||||
"@npm//@babel/core",
|
"@npm//@babel/core",
|
||||||
"@npm//@babel/types",
|
"@npm//@babel/types",
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,8 +5,9 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {Diagnostics} from '../../diagnostics';
|
||||||
import {FileUtils} from '../../file_utils';
|
|
||||||
import {OutputPathFn} from '../output_path';
|
import {OutputPathFn} from '../output_path';
|
||||||
import {TranslationBundle, TranslationHandler} from '../translator';
|
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.
|
* Translate an asset file by simply copying it to the appropriate translation output paths.
|
||||||
*/
|
*/
|
||||||
export class AssetTranslationHandler implements TranslationHandler {
|
export class AssetTranslationHandler implements TranslationHandler {
|
||||||
canTranslate(_relativeFilePath: string, _contents: Buffer): boolean {
|
constructor(private fs: FileSystem) {}
|
||||||
|
|
||||||
|
canTranslate(_relativeFilePath: PathSegment, _contents: Buffer): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(
|
translate(
|
||||||
diagnostics: Diagnostics, _sourceRoot: string, relativeFilePath: string, contents: Buffer,
|
diagnostics: Diagnostics, _sourceRoot: AbsoluteFsPath, relativeFilePath: PathSegment,
|
||||||
outputPathFn: OutputPathFn, translations: TranslationBundle[], sourceLocale?: string): void {
|
contents: Buffer, outputPathFn: OutputPathFn, translations: TranslationBundle[],
|
||||||
|
sourceLocale?: string): void {
|
||||||
for (const translation of translations) {
|
for (const translation of translations) {
|
||||||
try {
|
this.writeAssetFile(
|
||||||
FileUtils.writeFile(outputPathFn(translation.locale, relativeFilePath), contents);
|
diagnostics, outputPathFn, translation.locale, relativeFilePath, contents);
|
||||||
} catch (e) {
|
|
||||||
diagnostics.error(e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (sourceLocale !== undefined) {
|
if (sourceLocale !== undefined) {
|
||||||
|
this.writeAssetFile(diagnostics, outputPathFn, sourceLocale, relativeFilePath, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeAssetFile(
|
||||||
|
diagnostics: Diagnostics, outputPathFn: OutputPathFn, locale: string,
|
||||||
|
relativeFilePath: PathSegment, contents: Buffer): void {
|
||||||
try {
|
try {
|
||||||
FileUtils.writeFile(outputPathFn(sourceLocale, relativeFilePath), contents);
|
const outputPath = absoluteFrom(outputPathFn(locale, relativeFilePath));
|
||||||
|
this.fs.ensureDir(this.fs.dirname(outputPath));
|
||||||
|
this.fs.writeFile(outputPath, contents);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
diagnostics.error(e.message);
|
diagnostics.error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 * as glob from 'glob';
|
||||||
import {resolve} from 'path';
|
|
||||||
import * as yargs from 'yargs';
|
import * as yargs from 'yargs';
|
||||||
|
|
||||||
import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics';
|
import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics';
|
||||||
|
@ -66,8 +66,9 @@ if (require.main === module) {
|
||||||
.option('o', {
|
.option('o', {
|
||||||
alias: 'outputPath',
|
alias: 'outputPath',
|
||||||
required: true,
|
required: true,
|
||||||
describe:
|
describe: 'A output path pattern to where the translated files will be written.\n' +
|
||||||
'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}}`.'
|
'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', {
|
.option('m', {
|
||||||
|
@ -88,11 +89,13 @@ if (require.main === module) {
|
||||||
.help()
|
.help()
|
||||||
.parse(args);
|
.parse(args);
|
||||||
|
|
||||||
|
const fs = new NodeJSFileSystem();
|
||||||
|
setFileSystem(fs);
|
||||||
|
|
||||||
const sourceRootPath = options['r'];
|
const sourceRootPath = options['r'];
|
||||||
const sourceFilePaths =
|
const sourceFilePaths = glob.sync(options['s'], {cwd: sourceRootPath, nodir: true});
|
||||||
glob.sync(options['s'], {absolute: true, cwd: sourceRootPath, nodir: true});
|
|
||||||
const translationFilePaths: (string|string[])[] = convertArraysFromArgs(options['t']);
|
const translationFilePaths: (string|string[])[] = convertArraysFromArgs(options['t']);
|
||||||
const outputPathFn = getOutputPathFn(options['o']);
|
const outputPathFn = getOutputPathFn(fs.resolve(options['o']));
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const missingTranslation: DiagnosticHandlingStrategy = options['m'];
|
const missingTranslation: DiagnosticHandlingStrategy = options['m'];
|
||||||
const duplicateTranslation: DiagnosticHandlingStrategy = options['d'];
|
const duplicateTranslation: DiagnosticHandlingStrategy = options['d'];
|
||||||
|
@ -154,8 +157,9 @@ export interface TranslateFilesOptions {
|
||||||
*/
|
*/
|
||||||
translationFileLocales: (string|undefined)[];
|
translationFileLocales: (string|undefined)[];
|
||||||
/**
|
/**
|
||||||
* A function that computes the output path of where the translated files will be written.
|
* A function that computes the output path of where the translated files will be
|
||||||
* The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.
|
* written. The marker `{{LOCALE}}` will be replaced with the target locale. E.g.
|
||||||
|
* `dist/{{LOCALE}}`.
|
||||||
*/
|
*/
|
||||||
outputPathFn: OutputPathFn;
|
outputPathFn: OutputPathFn;
|
||||||
/**
|
/**
|
||||||
|
@ -189,7 +193,9 @@ export function translateFiles({
|
||||||
duplicateTranslation,
|
duplicateTranslation,
|
||||||
sourceLocale
|
sourceLocale
|
||||||
}: TranslateFilesOptions) {
|
}: TranslateFilesOptions) {
|
||||||
|
const fs = getFileSystem();
|
||||||
const translationLoader = new TranslationLoader(
|
const translationLoader = new TranslationLoader(
|
||||||
|
fs,
|
||||||
[
|
[
|
||||||
new Xliff2TranslationParser(),
|
new Xliff2TranslationParser(),
|
||||||
new Xliff1TranslationParser(),
|
new Xliff1TranslationParser(),
|
||||||
|
@ -199,21 +205,24 @@ export function translateFiles({
|
||||||
duplicateTranslation, diagnostics);
|
duplicateTranslation, diagnostics);
|
||||||
|
|
||||||
const resourceProcessor = new Translator(
|
const resourceProcessor = new Translator(
|
||||||
|
fs,
|
||||||
[
|
[
|
||||||
new SourceFileTranslationHandler({missingTranslation}),
|
new SourceFileTranslationHandler(fs, {missingTranslation}),
|
||||||
new AssetTranslationHandler(),
|
new AssetTranslationHandler(fs),
|
||||||
],
|
],
|
||||||
diagnostics);
|
diagnostics);
|
||||||
|
|
||||||
// Convert all the `translationFilePaths` elements to arrays.
|
// Convert all the `translationFilePaths` elements to arrays.
|
||||||
const translationFilePathsArrays =
|
const translationFilePathsArrays = translationFilePaths.map(
|
||||||
translationFilePaths.map(filePaths => Array.isArray(filePaths) ? filePaths : [filePaths]);
|
filePaths =>
|
||||||
|
Array.isArray(filePaths) ? filePaths.map(p => fs.resolve(p)) : [fs.resolve(filePaths)]);
|
||||||
|
|
||||||
const translations =
|
const translations =
|
||||||
translationLoader.loadBundles(translationFilePathsArrays, translationFileLocales);
|
translationLoader.loadBundles(translationFilePathsArrays, translationFileLocales);
|
||||||
sourceRootPath = resolve(sourceRootPath);
|
sourceRootPath = fs.resolve(sourceRootPath);
|
||||||
resourceProcessor.translateFiles(
|
resourceProcessor.translateFiles(
|
||||||
sourceFilePaths, sourceRootPath, outputPathFn, translations, sourceLocale);
|
sourceFilePaths.map(relativeFrom), fs.resolve(sourceRootPath), outputPathFn, translations,
|
||||||
|
sourceLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,8 +5,13 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
import {join} from 'path';
|
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 {
|
export interface OutputPathFn {
|
||||||
(locale: string, relativePath: string): string;
|
(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.
|
* 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.
|
* @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}}');
|
const [pre, post] = outputFolder.split('{{LOCALE}}');
|
||||||
return post === undefined ? (_locale, relativePath) => join(pre, relativePath) :
|
return post === undefined ? (_locale, relativePath) => join(pre, relativePath) :
|
||||||
(locale, relativePath) => join(pre + locale + post, relativePath);
|
(locale, relativePath) => join(pre + locale + post, relativePath);
|
||||||
|
|
|
@ -5,14 +5,15 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {parseSync, transformFromAstSync} from '@babel/core';
|
||||||
import {File, Program} from '@babel/types';
|
import {File, Program} from '@babel/types';
|
||||||
import {extname, join} from 'path';
|
|
||||||
import {Diagnostics} from '../../diagnostics';
|
import {Diagnostics} from '../../diagnostics';
|
||||||
import {FileUtils} from '../../file_utils';
|
|
||||||
import {TranslatePluginOptions} from '../../source_file_utils';
|
import {TranslatePluginOptions} from '../../source_file_utils';
|
||||||
import {OutputPathFn} from '../output_path';
|
import {OutputPathFn} from '../output_path';
|
||||||
import {TranslationBundle, TranslationHandler} from '../translator';
|
import {TranslationBundle, TranslationHandler} from '../translator';
|
||||||
|
|
||||||
import {makeEs2015TranslatePlugin} from './es2015_translate_plugin';
|
import {makeEs2015TranslatePlugin} from './es2015_translate_plugin';
|
||||||
import {makeEs5TranslatePlugin} from './es5_translate_plugin';
|
import {makeEs5TranslatePlugin} from './es5_translate_plugin';
|
||||||
import {makeLocalePlugin} from './locale_plugin';
|
import {makeLocalePlugin} from './locale_plugin';
|
||||||
|
@ -24,29 +25,32 @@ import {makeLocalePlugin} from './locale_plugin';
|
||||||
export class SourceFileTranslationHandler implements TranslationHandler {
|
export class SourceFileTranslationHandler implements TranslationHandler {
|
||||||
private sourceLocaleOptions:
|
private sourceLocaleOptions:
|
||||||
TranslatePluginOptions = {...this.translationOptions, missingTranslation: 'ignore'};
|
TranslatePluginOptions = {...this.translationOptions, missingTranslation: 'ignore'};
|
||||||
constructor(private translationOptions: TranslatePluginOptions = {}) {}
|
constructor(private fs: FileSystem, private translationOptions: TranslatePluginOptions = {}) {}
|
||||||
|
|
||||||
canTranslate(relativeFilePath: string, _contents: Buffer): boolean {
|
canTranslate(relativeFilePath: PathSegment, _contents: Buffer): boolean {
|
||||||
return extname(relativeFilePath) === '.js';
|
return this.fs.extname(relativeFrom(relativeFilePath)) === '.js';
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(
|
translate(
|
||||||
diagnostics: Diagnostics, sourceRoot: string, relativeFilePath: string, contents: Buffer,
|
diagnostics: Diagnostics, sourceRoot: AbsoluteFsPath, relativeFilePath: PathSegment,
|
||||||
outputPathFn: OutputPathFn, translations: TranslationBundle[], sourceLocale?: string): void {
|
contents: Buffer, outputPathFn: OutputPathFn, translations: TranslationBundle[],
|
||||||
|
sourceLocale?: string): void {
|
||||||
const sourceCode = contents.toString('utf8');
|
const sourceCode = contents.toString('utf8');
|
||||||
// A short-circuit check to avoid parsing the file into an AST if it does not contain any
|
// A short-circuit check to avoid parsing the file into an AST if it does not contain any
|
||||||
// `$localize` identifiers.
|
// `$localize` identifiers.
|
||||||
if (!sourceCode.includes('$localize')) {
|
if (!sourceCode.includes('$localize')) {
|
||||||
for (const translation of translations) {
|
for (const translation of translations) {
|
||||||
FileUtils.writeFile(outputPathFn(translation.locale, relativeFilePath), contents);
|
this.writeSourceFile(
|
||||||
|
diagnostics, outputPathFn, translation.locale, relativeFilePath, contents);
|
||||||
}
|
}
|
||||||
if (sourceLocale !== undefined) {
|
if (sourceLocale !== undefined) {
|
||||||
FileUtils.writeFile(outputPathFn(sourceLocale, relativeFilePath), contents);
|
this.writeSourceFile(diagnostics, outputPathFn, sourceLocale, relativeFilePath, contents);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const ast = parseSync(sourceCode, {sourceRoot, filename: relativeFilePath});
|
const ast = parseSync(sourceCode, {sourceRoot, filename: relativeFilePath});
|
||||||
if (!ast) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// Output a translated copy of the file for each locale.
|
// Output a translated copy of the file for each locale.
|
||||||
|
@ -67,7 +71,7 @@ export class SourceFileTranslationHandler implements TranslationHandler {
|
||||||
|
|
||||||
private translateFile(
|
private translateFile(
|
||||||
diagnostics: Diagnostics, ast: File|Program, translationBundle: TranslationBundle,
|
diagnostics: Diagnostics, ast: File|Program, translationBundle: TranslationBundle,
|
||||||
sourceRoot: string, filename: string, outputPathFn: OutputPathFn,
|
sourceRoot: AbsoluteFsPath, filename: PathSegment, outputPathFn: OutputPathFn,
|
||||||
options: TranslatePluginOptions) {
|
options: TranslatePluginOptions) {
|
||||||
const translated = transformFromAstSync(ast, undefined, {
|
const translated = transformFromAstSync(ast, undefined, {
|
||||||
compact: true,
|
compact: true,
|
||||||
|
@ -80,10 +84,26 @@ export class SourceFileTranslationHandler implements TranslationHandler {
|
||||||
filename,
|
filename,
|
||||||
});
|
});
|
||||||
if (translated && translated.code) {
|
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 {
|
} else {
|
||||||
diagnostics.error(`Unable to translate source file: ${join(sourceRoot, filename)}`);
|
diagnostics.error(`Unable to translate source file: ${this.fs.join(sourceRoot, filename)}`);
|
||||||
return;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {DiagnosticHandlingStrategy, Diagnostics} from '../../diagnostics';
|
||||||
import {FileUtils} from '../../file_utils';
|
|
||||||
import {TranslationBundle} from '../translator';
|
import {TranslationBundle} from '../translator';
|
||||||
|
|
||||||
import {TranslationParser} from './translation_parsers/translation_parser';
|
import {TranslationParser} from './translation_parsers/translation_parser';
|
||||||
|
@ -16,7 +16,7 @@ import {TranslationParser} from './translation_parsers/translation_parser';
|
||||||
*/
|
*/
|
||||||
export class TranslationLoader {
|
export class TranslationLoader {
|
||||||
constructor(
|
constructor(
|
||||||
private translationParsers: TranslationParser<any>[],
|
private fs: FileSystem, private translationParsers: TranslationParser<any>[],
|
||||||
private duplicateTranslation: DiagnosticHandlingStrategy,
|
private duplicateTranslation: DiagnosticHandlingStrategy,
|
||||||
/** @deprecated */ private diagnostics?: Diagnostics) {}
|
/** @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
|
* 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.
|
* same, then a warning is reported.
|
||||||
*/
|
*/
|
||||||
loadBundles(translationFilePaths: string[][], translationFileLocales: (string|undefined)[]):
|
loadBundles(
|
||||||
TranslationBundle[] {
|
translationFilePaths: AbsoluteFsPath[][],
|
||||||
|
translationFileLocales: (string|undefined)[]): TranslationBundle[] {
|
||||||
return translationFilePaths.map((filePaths, index) => {
|
return translationFilePaths.map((filePaths, index) => {
|
||||||
const providedLocale = translationFileLocales[index];
|
const providedLocale = translationFileLocales[index];
|
||||||
return this.mergeBundles(filePaths, providedLocale);
|
return this.mergeBundles(filePaths, providedLocale);
|
||||||
|
@ -53,8 +54,9 @@ export class TranslationLoader {
|
||||||
/**
|
/**
|
||||||
* Load all the translations from the file at the given `filePath`.
|
* Load all the translations from the file at the given `filePath`.
|
||||||
*/
|
*/
|
||||||
private loadBundle(filePath: string, providedLocale: string|undefined): TranslationBundle {
|
private loadBundle(filePath: AbsoluteFsPath, providedLocale: string|undefined):
|
||||||
const fileContents = FileUtils.readFile(filePath);
|
TranslationBundle {
|
||||||
|
const fileContents = this.fs.readFile(filePath);
|
||||||
for (const translationParser of this.translationParsers) {
|
for (const translationParser of this.translationParsers) {
|
||||||
const result = translationParser.canParse(filePath, fileContents);
|
const result = translationParser.canParse(filePath, fileContents);
|
||||||
if (!result) {
|
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
|
* There is more than one `filePath` for this locale, so load each as a bundle and then merge them
|
||||||
* all together.
|
* 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 bundles = filePaths.map(filePath => this.loadBundle(filePath, providedLocale));
|
||||||
const bundle = bundles[0];
|
const bundle = bundles[0];
|
||||||
for (let i = 1; i < bundles.length; i++) {
|
for (let i = 1; i < bundles.length; i++) {
|
||||||
|
|
|
@ -5,11 +5,10 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {ɵMessageId, ɵParsedTranslation} from '@angular/localize';
|
||||||
import {relative} from 'path';
|
|
||||||
|
|
||||||
import {Diagnostics} from '../diagnostics';
|
import {Diagnostics} from '../diagnostics';
|
||||||
import {FileUtils} from '../file_utils';
|
|
||||||
|
|
||||||
import {OutputPathFn} from './output_path';
|
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 relativeFilePath A relative path from the sourceRoot to the resource file to handle.
|
||||||
* @param contents The contents of the 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`,
|
* Translate the file at `relativeFilePath` containing `contents`, using the given `translations`,
|
||||||
|
@ -54,8 +53,9 @@ export interface TranslationHandler {
|
||||||
* stripped out.
|
* stripped out.
|
||||||
*/
|
*/
|
||||||
translate(
|
translate(
|
||||||
diagnostics: Diagnostics, sourceRoot: string, relativeFilePath: string, contents: Buffer,
|
diagnostics: Diagnostics, sourceRoot: AbsoluteFsPath, relativeFilePath: PathSegment,
|
||||||
outputPathFn: OutputPathFn, translations: TranslationBundle[], sourceLocale?: string): void;
|
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()`.
|
* The file will be translated by the first handler that returns true for `canTranslate()`.
|
||||||
*/
|
*/
|
||||||
export class Translator {
|
export class Translator {
|
||||||
constructor(private resourceHandlers: TranslationHandler[], private diagnostics: Diagnostics) {}
|
constructor(
|
||||||
|
private fs: FileSystem, private resourceHandlers: TranslationHandler[],
|
||||||
|
private diagnostics: Diagnostics) {}
|
||||||
|
|
||||||
translateFiles(
|
translateFiles(
|
||||||
inputPaths: string[], rootPath: string, outputPathFn: OutputPathFn,
|
inputPaths: PathSegment[], rootPath: AbsoluteFsPath, outputPathFn: OutputPathFn,
|
||||||
translations: TranslationBundle[], sourceLocale?: string): void {
|
translations: TranslationBundle[], sourceLocale?: string): void {
|
||||||
inputPaths.forEach(inputPath => {
|
inputPaths.forEach(inputPath => {
|
||||||
const contents = FileUtils.readFileBuffer(inputPath);
|
const absInputPath = this.fs.resolve(rootPath, inputPath);
|
||||||
const relativePath = relative(rootPath, inputPath);
|
const contents = this.fs.readFileBuffer(absInputPath);
|
||||||
|
const relativePath = this.fs.relative(rootPath, absInputPath);
|
||||||
for (const resourceHandler of this.resourceHandlers) {
|
for (const resourceHandler of this.resourceHandlers) {
|
||||||
if (resourceHandler.canTranslate(relativePath, contents)) {
|
if (resourceHandler.canTranslate(relativePath, contents)) {
|
||||||
return resourceHandler.translate(
|
return resourceHandler.translate(
|
||||||
|
|
|
@ -9,6 +9,8 @@ ts_library(
|
||||||
deps = [
|
deps = [
|
||||||
"//packages:types",
|
"//packages:types",
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/file_system/testing",
|
||||||
"//packages/localize",
|
"//packages/localize",
|
||||||
"//packages/localize/src/tools",
|
"//packages/localize/src/tools",
|
||||||
"@npm//@babel/core",
|
"@npm//@babel/core",
|
||||||
|
|
|
@ -5,52 +5,65 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {Diagnostics} from '../../../src/diagnostics';
|
||||||
import {FileUtils} from '../../../src/file_utils';
|
|
||||||
import {AssetTranslationHandler} from '../../../src/translate/asset_files/asset_translation_handler';
|
import {AssetTranslationHandler} from '../../../src/translate/asset_files/asset_translation_handler';
|
||||||
import {TranslationBundle} from '../../../src/translate/translator';
|
import {TranslationBundle} from '../../../src/translate/translator';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
describe('AssetTranslationHandler', () => {
|
describe('AssetTranslationHandler', () => {
|
||||||
|
let fs: FileSystem;
|
||||||
|
let rootPath: AbsoluteFsPath;
|
||||||
|
let filePath: PathSegment;
|
||||||
|
let enTranslationPath: AbsoluteFsPath;
|
||||||
|
let enUSTranslationPath: AbsoluteFsPath;
|
||||||
|
let frTranslationPath: AbsoluteFsPath;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
describe('canTranslate()', () => {
|
describe('canTranslate()', () => {
|
||||||
it('should always return true', () => {
|
it('should always return true', () => {
|
||||||
const handler = new AssetTranslationHandler();
|
const handler = new AssetTranslationHandler(fs);
|
||||||
expect(handler.canTranslate('relative/path', Buffer.from('contents'))).toBe(true);
|
expect(handler.canTranslate(filePath, Buffer.from('contents'))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('translate()', () => {
|
describe('translate()', () => {
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(FileUtils, 'writeFile');
|
|
||||||
spyOn(FileUtils, 'ensureDir');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should write the translated file for each translation locale', () => {
|
it('should write the translated file for each translation locale', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new AssetTranslationHandler();
|
const handler = new AssetTranslationHandler(fs);
|
||||||
const translations = [
|
const translations = [
|
||||||
{locale: 'en', translations: {}},
|
{locale: 'en', translations: {}},
|
||||||
{locale: 'fr', translations: {}},
|
{locale: 'fr', translations: {}},
|
||||||
];
|
];
|
||||||
const contents = Buffer.from('contents');
|
const contents = Buffer.from('contents');
|
||||||
handler.translate(
|
handler.translate(
|
||||||
diagnostics, '/root/path', 'relative/path', contents, mockOutputPathFn, translations);
|
diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations);
|
||||||
|
|
||||||
expect(FileUtils.writeFile).toHaveBeenCalledWith('/translations/en/relative/path', contents);
|
expect(fs.readFileBuffer(enTranslationPath)).toEqual(contents);
|
||||||
expect(FileUtils.writeFile).toHaveBeenCalledWith('/translations/fr/relative/path', contents);
|
expect(fs.readFileBuffer(frTranslationPath)).toEqual(contents);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should write the translated file to the source locale if provided', () => {
|
it('should write the translated file to the source locale if provided', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new AssetTranslationHandler();
|
const handler = new AssetTranslationHandler(fs);
|
||||||
const translations: TranslationBundle[] = [];
|
const translations: TranslationBundle[] = [];
|
||||||
const contents = Buffer.from('contents');
|
const contents = Buffer.from('contents');
|
||||||
const sourceLocale = 'en-US';
|
const sourceLocale = 'en-US';
|
||||||
handler.translate(
|
handler.translate(
|
||||||
diagnostics, '/root/path', 'relative/path', contents, mockOutputPathFn, translations,
|
diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations,
|
||||||
sourceLocale);
|
sourceLocale);
|
||||||
|
|
||||||
expect(FileUtils.writeFile)
|
expect(fs.readFileBuffer(enUSTranslationPath)).toEqual(contents);
|
||||||
.toHaveBeenCalledWith('/translations/en-US/relative/path', contents);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -58,3 +71,4 @@ describe('AssetTranslationHandler', () => {
|
||||||
function mockOutputPathFn(locale: string, relativePath: string) {
|
function mockOutputPathFn(locale: string, relativePath: string) {
|
||||||
return `/translations/${locale}/${relativePath}`;
|
return `/translations/${locale}/${relativePath}`;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -8,6 +8,9 @@ ts_library(
|
||||||
),
|
),
|
||||||
deps = [
|
deps = [
|
||||||
"//packages:types",
|
"//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",
|
"//packages/localize/src/tools",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,33 +5,41 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {Diagnostics} from '../../../src/diagnostics';
|
||||||
import {FileUtils} from '../../../src/file_utils';
|
|
||||||
import {translateFiles} from '../../../src/translate/main';
|
import {translateFiles} from '../../../src/translate/main';
|
||||||
import {getOutputPathFn} from '../../../src/translate/output_path';
|
import {getOutputPathFn} from '../../../src/translate/output_path';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
describe('translateFiles()', () => {
|
describe('translateFiles()', () => {
|
||||||
const tmpDir = process.env.TEST_TMPDIR;
|
let fs: FileSystem;
|
||||||
if (tmpDir === undefined) return;
|
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));
|
testFilesDir = fs.resolve(testDir, 'test_files');
|
||||||
afterEach(() => {
|
loadTestDirectory(fs, realResolve(__dirname, 'test_files'), testFilesDir);
|
||||||
FileUtils.remove(testDir);
|
translationFilesDir = fs.resolve(testDir, 'test_files');
|
||||||
|
loadTestDirectory(fs, realResolve(__dirname, 'locales'), translationFilesDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should copy non-code files to the destination folders', () => {
|
it('should copy non-code files to the destination folders', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}'));
|
const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}'));
|
||||||
translateFiles({
|
translateFiles({
|
||||||
sourceRootPath: resolve(__dirname, 'test_files'),
|
sourceRootPath: testFilesDir,
|
||||||
sourceFilePaths: resolveAll(__dirname + '/test_files', ['test-1.txt', 'test-2.txt']),
|
sourceFilePaths: ['test-1.txt', 'test-2.txt'],
|
||||||
outputPathFn,
|
outputPathFn,
|
||||||
translationFilePaths: resolveAll(
|
translationFilePaths: resolveAll(
|
||||||
__dirname + '/locales',
|
translationFilesDir,
|
||||||
['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']),
|
['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']),
|
||||||
translationFileLocales: [],
|
translationFileLocales: [],
|
||||||
diagnostics,
|
diagnostics,
|
||||||
|
@ -41,33 +49,33 @@ describe('translateFiles()', () => {
|
||||||
|
|
||||||
expect(diagnostics.messages.length).toEqual(0);
|
expect(diagnostics.messages.length).toEqual(0);
|
||||||
|
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'fr', 'test-1.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'fr', 'test-1.txt')))
|
||||||
.toEqual('Contents of test-1.txt');
|
.toEqual('Contents of test-1.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'fr', 'test-2.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'fr', 'test-2.txt')))
|
||||||
.toEqual('Contents of test-2.txt');
|
.toEqual('Contents of test-2.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'de', 'test-1.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'de', 'test-1.txt')))
|
||||||
.toEqual('Contents of test-1.txt');
|
.toEqual('Contents of test-1.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'de', 'test-2.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'de', 'test-2.txt')))
|
||||||
.toEqual('Contents of test-2.txt');
|
.toEqual('Contents of test-2.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'es', 'test-1.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'es', 'test-1.txt')))
|
||||||
.toEqual('Contents of test-1.txt');
|
.toEqual('Contents of test-1.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'es', 'test-2.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'es', 'test-2.txt')))
|
||||||
.toEqual('Contents of test-2.txt');
|
.toEqual('Contents of test-2.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'it', 'test-1.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'it', 'test-1.txt')))
|
||||||
.toEqual('Contents of test-1.txt');
|
.toEqual('Contents of test-1.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'it', 'test-2.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'it', 'test-2.txt')))
|
||||||
.toEqual('Contents of test-2.txt');
|
.toEqual('Contents of test-2.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should translate and copy source-code files to the destination folders', () => {
|
it('should translate and copy source-code files to the destination folders', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}'));
|
const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}'));
|
||||||
translateFiles({
|
translateFiles({
|
||||||
sourceRootPath: resolve(__dirname, 'test_files'),
|
sourceRootPath: testFilesDir,
|
||||||
sourceFilePaths: resolveAll(__dirname + '/test_files', ['test.js']),
|
sourceFilePaths: ['test.js'],
|
||||||
outputPathFn,
|
outputPathFn,
|
||||||
translationFilePaths: resolveAll(
|
translationFilePaths: resolveAll(
|
||||||
__dirname + '/locales',
|
translationFilesDir,
|
||||||
['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']),
|
['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']),
|
||||||
translationFileLocales: [],
|
translationFileLocales: [],
|
||||||
diagnostics,
|
diagnostics,
|
||||||
|
@ -77,25 +85,25 @@ describe('translateFiles()', () => {
|
||||||
|
|
||||||
expect(diagnostics.messages.length).toEqual(0);
|
expect(diagnostics.messages.length).toEqual(0);
|
||||||
|
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'fr', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'fr', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Bonjour, "+name+"!";`);
|
.toEqual(`var name="World";var message="Bonjour, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'de', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'de', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Guten Tag, "+name+"!";`);
|
.toEqual(`var name="World";var message="Guten Tag, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'es', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'es', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Hola, "+name+"!";`);
|
.toEqual(`var name="World";var message="Hola, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'it', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'it', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Ciao, "+name+"!";`);
|
.toEqual(`var name="World";var message="Ciao, "+name+"!";`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should translate and copy source-code files overriding the locales', () => {
|
it('should translate and copy source-code files overriding the locales', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}'));
|
const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}'));
|
||||||
translateFiles({
|
translateFiles({
|
||||||
sourceRootPath: resolve(__dirname, 'test_files'),
|
sourceRootPath: testFilesDir,
|
||||||
sourceFilePaths: resolveAll(__dirname + '/test_files', ['test.js']),
|
sourceFilePaths: ['test.js'],
|
||||||
outputPathFn,
|
outputPathFn,
|
||||||
translationFilePaths: resolveAll(
|
translationFilePaths: resolveAll(
|
||||||
__dirname + '/locales',
|
translationFilesDir,
|
||||||
['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']),
|
['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']),
|
||||||
translationFileLocales: ['xde', undefined, 'fr'],
|
translationFileLocales: ['xde', undefined, 'fr'],
|
||||||
diagnostics,
|
diagnostics,
|
||||||
|
@ -108,28 +116,28 @@ describe('translateFiles()', () => {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message:
|
message:
|
||||||
`The provided locale "xde" does not match the target locale "de" found in the translation file "${
|
`The provided locale "xde" does not match the target locale "de" found in the translation file "${
|
||||||
resolve(__dirname, 'locales', 'messages.de.json')}".`
|
fs.resolve(translationFilesDir, 'messages.de.json')}".`
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'xde', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'xde', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Guten Tag, "+name+"!";`);
|
.toEqual(`var name="World";var message="Guten Tag, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'es', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'es', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Hola, "+name+"!";`);
|
.toEqual(`var name="World";var message="Hola, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'fr', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'fr', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Bonjour, "+name+"!";`);
|
.toEqual(`var name="World";var message="Bonjour, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'it', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'it', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Ciao, "+name+"!";`);
|
.toEqual(`var name="World";var message="Ciao, "+name+"!";`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should merge translation files, if more than one provided, and translate source-code', () => {
|
it('should merge translation files, if more than one provided, and translate source-code', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}'));
|
const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}'));
|
||||||
translateFiles({
|
translateFiles({
|
||||||
sourceRootPath: resolve(__dirname, 'test_files'),
|
sourceRootPath: testFilesDir,
|
||||||
sourceFilePaths: resolveAll(__dirname + '/test_files', ['test-extra.js']),
|
sourceFilePaths: ['test-extra.js'],
|
||||||
outputPathFn,
|
outputPathFn,
|
||||||
translationFilePaths: resolveAllRecursive(
|
translationFilePaths: resolveAllRecursive(
|
||||||
__dirname + '/locales',
|
translationFilesDir,
|
||||||
[['messages.de.json', 'messages-extra.de.json'], 'messages.es.xlf']),
|
[['messages.de.json', 'messages-extra.de.json'], 'messages.es.xlf']),
|
||||||
translationFileLocales: [],
|
translationFileLocales: [],
|
||||||
diagnostics,
|
diagnostics,
|
||||||
|
@ -146,25 +154,24 @@ describe('translateFiles()', () => {
|
||||||
|
|
||||||
// The `de` locale translates the `customExtra` message because it is in the
|
// The `de` locale translates the `customExtra` message because it is in the
|
||||||
// `messages-extra.de.json` file that was merged.
|
// `messages-extra.de.json` file that was merged.
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'de', 'test-extra.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'de', 'test-extra.js')))
|
||||||
.toEqual(
|
.toEqual(
|
||||||
`var name="World";var message="Guten Tag, "+name+"!";var message="Auf wiedersehen, "+name+"!";`);
|
`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.
|
// The `es` locale does not translate `customExtra` because there is no translation for it.
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'es', 'test-extra.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'es', 'test-extra.js')))
|
||||||
.toEqual(
|
.toEqual(
|
||||||
`var name="World";var message="Hola, "+name+"!";var message="Goodbye, "+name+"!";`);
|
`var name="World";var message="Hola, "+name+"!";var message="Goodbye, "+name+"!";`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform and/or copy files to the destination folders', () => {
|
it('should transform and/or copy files to the destination folders', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}'));
|
const outputPathFn = getOutputPathFn(fs.resolve(testDir, '{{LOCALE}}'));
|
||||||
translateFiles({
|
translateFiles({
|
||||||
sourceRootPath: resolve(__dirname, 'test_files'),
|
sourceRootPath: testFilesDir,
|
||||||
sourceFilePaths:
|
sourceFilePaths: ['test-1.txt', 'test-2.txt', 'test.js'],
|
||||||
resolveAll(__dirname + '/test_files', ['test-1.txt', 'test-2.txt', 'test.js']),
|
|
||||||
outputPathFn,
|
outputPathFn,
|
||||||
translationFilePaths: resolveAll(
|
translationFilePaths: resolveAll(
|
||||||
__dirname + '/locales',
|
translationFilesDir,
|
||||||
['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']),
|
['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf', 'messages.it.xtb']),
|
||||||
translationFileLocales: [],
|
translationFileLocales: [],
|
||||||
diagnostics,
|
diagnostics,
|
||||||
|
@ -174,39 +181,40 @@ describe('translateFiles()', () => {
|
||||||
|
|
||||||
expect(diagnostics.messages.length).toEqual(0);
|
expect(diagnostics.messages.length).toEqual(0);
|
||||||
|
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'fr', 'test-1.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'fr', 'test-1.txt')))
|
||||||
.toEqual('Contents of test-1.txt');
|
.toEqual('Contents of test-1.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'fr', 'test-2.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'fr', 'test-2.txt')))
|
||||||
.toEqual('Contents of test-2.txt');
|
.toEqual('Contents of test-2.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'de', 'test-1.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'de', 'test-1.txt')))
|
||||||
.toEqual('Contents of test-1.txt');
|
.toEqual('Contents of test-1.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'de', 'test-2.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'de', 'test-2.txt')))
|
||||||
.toEqual('Contents of test-2.txt');
|
.toEqual('Contents of test-2.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'es', 'test-1.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'es', 'test-1.txt')))
|
||||||
.toEqual('Contents of test-1.txt');
|
.toEqual('Contents of test-1.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'es', 'test-2.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'es', 'test-2.txt')))
|
||||||
.toEqual('Contents of test-2.txt');
|
.toEqual('Contents of test-2.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'it', 'test-1.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'it', 'test-1.txt')))
|
||||||
.toEqual('Contents of test-1.txt');
|
.toEqual('Contents of test-1.txt');
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'it', 'test-2.txt')))
|
expect(fs.readFile(fs.resolve(testDir, 'it', 'test-2.txt')))
|
||||||
.toEqual('Contents of test-2.txt');
|
.toEqual('Contents of test-2.txt');
|
||||||
|
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'fr', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'fr', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Bonjour, "+name+"!";`);
|
.toEqual(`var name="World";var message="Bonjour, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'de', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'de', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Guten Tag, "+name+"!";`);
|
.toEqual(`var name="World";var message="Guten Tag, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'es', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'es', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Hola, "+name+"!";`);
|
.toEqual(`var name="World";var message="Hola, "+name+"!";`);
|
||||||
expect(FileUtils.readFile(resolve(testDir, 'it', 'test.js')))
|
expect(fs.readFile(fs.resolve(testDir, 'it', 'test.js')))
|
||||||
.toEqual(`var name="World";var message="Ciao, "+name+"!";`);
|
.toEqual(`var name="World";var message="Ciao, "+name+"!";`);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
function resolveAll(rootPath: string, paths: string[]): string[] {
|
function resolveAll(rootPath: string, paths: string[]): string[] {
|
||||||
return paths.map(p => resolve(rootPath, p));
|
return paths.map(p => fs.resolve(rootPath, p));
|
||||||
}
|
}
|
||||||
|
function resolveAllRecursive(
|
||||||
function resolveAllRecursive(rootPath: string, paths: (string|string[])[]): (string|string[])[] {
|
rootPath: string, paths: (string|string[])[]): (string|string[])[] {
|
||||||
return paths.map(
|
return paths.map(
|
||||||
p => Array.isArray(p) ? p.map(p2 => resolve(rootPath, p2)) : resolve(rootPath, p));
|
p => Array.isArray(p) ? p.map(p2 => fs.resolve(rootPath, p2)) : fs.resolve(rootPath, p));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -5,40 +5,38 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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';
|
import {getOutputPathFn} from '../../src/translate/output_path';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
describe('getOutputPathFn()', () => {
|
describe('getOutputPathFn()', () => {
|
||||||
it('should return a function that joins the `outputPath` and the `relativePath`', () => {
|
it('should return a function that joins the `outputPath` and the `relativePath`', () => {
|
||||||
const fn = getOutputPathFn('/output/path');
|
const fn = getOutputPathFn(absoluteFrom('/output/path'));
|
||||||
expect(fn('en', 'relative/path')).toEqual('/output/path/relative/path');
|
expect(fn('en', 'relative/path')).toEqual(absoluteFrom('/output/path/relative/path'));
|
||||||
expect(fn('en', '../parent/path')).toEqual('/output/parent/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`',
|
it('should return a function that interpolates the `{{LOCALE}}` in the middle of the `outputPath`',
|
||||||
() => {
|
() => {
|
||||||
const fn = getOutputPathFn('/output/{{LOCALE}}/path');
|
const fn = getOutputPathFn(absoluteFrom('/output/{{LOCALE}}/path'));
|
||||||
expect(fn('en', 'relative/path')).toEqual('/output/en/path/relative/path');
|
expect(fn('en', 'relative/path')).toEqual(absoluteFrom('/output/en/path/relative/path'));
|
||||||
expect(fn('fr', 'relative/path')).toEqual('/output/fr/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`',
|
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');
|
const fn = getOutputPathFn(absoluteFrom('/output-{{LOCALE}}-path'));
|
||||||
expect(fn('en', 'relative/path')).toEqual('/output-en-path/relative/path');
|
expect(fn('en', 'relative/path')).toEqual(absoluteFrom('/output-en-path/relative/path'));
|
||||||
expect(fn('fr', 'relative/path')).toEqual('/output-fr-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 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`',
|
it('should return a function that interpolates the `{{LOCALE}}` at the end of the `outputPath`',
|
||||||
() => {
|
() => {
|
||||||
const fn = getOutputPathFn('/output/{{LOCALE}}');
|
const fn = getOutputPathFn(absoluteFrom('/output/{{LOCALE}}'));
|
||||||
expect(fn('en', 'relative/path')).toEqual('/output/en/relative/path');
|
expect(fn('en', 'relative/path')).toEqual(absoluteFrom('/output/en/relative/path'));
|
||||||
expect(fn('fr', 'relative/path')).toEqual('/output/fr/relative/path');
|
expect(fn('fr', 'relative/path')).toEqual(absoluteFrom('/output/fr/relative/path'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -5,59 +5,71 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {Diagnostics} from '../../../src/diagnostics';
|
||||||
import {FileUtils} from '../../../src/file_utils';
|
|
||||||
import {SourceFileTranslationHandler} from '../../../src/translate/source_files/source_file_translation_handler';
|
import {SourceFileTranslationHandler} from '../../../src/translate/source_files/source_file_translation_handler';
|
||||||
import {TranslationBundle} from '../../../src/translate/translator';
|
import {TranslationBundle} from '../../../src/translate/translator';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
describe('SourceFileTranslationHandler', () => {
|
describe('SourceFileTranslationHandler', () => {
|
||||||
|
let fs: FileSystem;
|
||||||
|
let rootPath: AbsoluteFsPath;
|
||||||
|
let filePath: PathSegment;
|
||||||
|
let enTranslationPath: AbsoluteFsPath;
|
||||||
|
let enUSTranslationPath: AbsoluteFsPath;
|
||||||
|
let frTranslationPath: AbsoluteFsPath;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
describe('canTranslate()', () => {
|
describe('canTranslate()', () => {
|
||||||
it('should return true if the path ends in ".js"', () => {
|
it('should return true if the path ends in ".js"', () => {
|
||||||
const handler = new SourceFileTranslationHandler();
|
const handler = new SourceFileTranslationHandler(fs);
|
||||||
expect(handler.canTranslate('relative/path', Buffer.from('contents'))).toBe(false);
|
expect(handler.canTranslate(relativeFrom('relative/path'), Buffer.from('contents')))
|
||||||
expect(handler.canTranslate('relative/path.js', Buffer.from('contents'))).toBe(true);
|
.toBe(false);
|
||||||
|
expect(handler.canTranslate(filePath, Buffer.from('contents'))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('translate()', () => {
|
describe('translate()', () => {
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(FileUtils, 'writeFile');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should copy files for each translation locale if they contain no reference to `$localize`',
|
it('should copy files for each translation locale if they contain no reference to `$localize`',
|
||||||
() => {
|
() => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new SourceFileTranslationHandler();
|
const handler = new SourceFileTranslationHandler(fs);
|
||||||
const translations = [
|
const translations = [
|
||||||
{locale: 'en', translations: {}},
|
{locale: 'en', translations: {}},
|
||||||
{locale: 'fr', translations: {}},
|
{locale: 'fr', translations: {}},
|
||||||
];
|
];
|
||||||
const contents = Buffer.from('contents');
|
const contents = Buffer.from('contents');
|
||||||
handler.translate(
|
handler.translate(
|
||||||
diagnostics, '/root/path', 'relative/path', contents, mockOutputPathFn, translations);
|
diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations);
|
||||||
|
|
||||||
expect(FileUtils.writeFile)
|
expect(fs.readFileBuffer(enTranslationPath)).toEqual(contents);
|
||||||
.toHaveBeenCalledWith('/translations/en/relative/path', contents);
|
expect(fs.readFileBuffer(frTranslationPath)).toEqual(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',
|
it('should copy files to the source locale if they contain no reference to `$localize` and `sourceLocale` is provided',
|
||||||
() => {
|
() => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new SourceFileTranslationHandler();
|
const handler = new SourceFileTranslationHandler(fs);
|
||||||
const translations: TranslationBundle[] = [];
|
const translations: TranslationBundle[] = [];
|
||||||
const contents = Buffer.from('contents');
|
const contents = Buffer.from('contents');
|
||||||
handler.translate(
|
handler.translate(
|
||||||
diagnostics, '/root/path', 'relative/path', contents, mockOutputPathFn, translations,
|
diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations, 'en-US');
|
||||||
'en-US');
|
expect(fs.readFileBuffer(enUSTranslationPath)).toEqual(contents);
|
||||||
expect(FileUtils.writeFile)
|
|
||||||
.toHaveBeenCalledWith('/translations/en-US/relative/path', contents);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform each $localize template tag', () => {
|
it('should transform each $localize template tag', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new SourceFileTranslationHandler();
|
const handler = new SourceFileTranslationHandler(fs);
|
||||||
const translations = [
|
const translations = [
|
||||||
{locale: 'en', translations: {}},
|
{locale: 'en', translations: {}},
|
||||||
{locale: 'fr', translations: {}},
|
{locale: 'fr', translations: {}},
|
||||||
|
@ -67,32 +79,30 @@ describe('SourceFileTranslationHandler', () => {
|
||||||
'$localize(__makeTemplateObject(["a", "b", "c"], ["a", "b", "c"]), 1, 2);');
|
'$localize(__makeTemplateObject(["a", "b", "c"], ["a", "b", "c"]), 1, 2);');
|
||||||
const output = '"a"+1+"b"+2+"c";"a"+1+"b"+2+"c";';
|
const output = '"a"+1+"b"+2+"c";"a"+1+"b"+2+"c";';
|
||||||
handler.translate(
|
handler.translate(
|
||||||
diagnostics, '/root/path', 'relative/path.js', contents, mockOutputPathFn, translations);
|
diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations);
|
||||||
|
|
||||||
expect(FileUtils.writeFile).toHaveBeenCalledWith('/translations/en/relative/path.js', output);
|
expect(fs.readFile(enTranslationPath)).toEqual(output);
|
||||||
expect(FileUtils.writeFile).toHaveBeenCalledWith('/translations/fr/relative/path.js', output);
|
expect(fs.readFile(frTranslationPath)).toEqual(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform each $localize template tag and write it to the source locale if provided',
|
it('should transform each $localize template tag and write it to the source locale if provided',
|
||||||
() => {
|
() => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new SourceFileTranslationHandler();
|
const handler = new SourceFileTranslationHandler(fs);
|
||||||
const translations: TranslationBundle[] = [];
|
const translations: TranslationBundle[] = [];
|
||||||
const contents = Buffer.from(
|
const contents = Buffer.from(
|
||||||
'$localize`a${1}b${2}c`;\n' +
|
'$localize`a${1}b${2}c`;\n' +
|
||||||
'$localize(__makeTemplateObject(["a", "b", "c"], ["a", "b", "c"]), 1, 2);');
|
'$localize(__makeTemplateObject(["a", "b", "c"], ["a", "b", "c"]), 1, 2);');
|
||||||
const output = '"a"+1+"b"+2+"c";"a"+1+"b"+2+"c";';
|
const output = '"a"+1+"b"+2+"c";"a"+1+"b"+2+"c";';
|
||||||
handler.translate(
|
handler.translate(
|
||||||
diagnostics, '/root/path', 'relative/path.js', contents, mockOutputPathFn,
|
diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations, 'en-US');
|
||||||
translations, 'en-US');
|
|
||||||
|
|
||||||
expect(FileUtils.writeFile)
|
expect(fs.readFile(enUSTranslationPath)).toEqual(output);
|
||||||
.toHaveBeenCalledWith('/translations/en-US/relative/path.js', output);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform `$localize.locale` identifiers', () => {
|
it('should transform `$localize.locale` identifiers', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new SourceFileTranslationHandler();
|
const handler = new SourceFileTranslationHandler(fs);
|
||||||
const translations: TranslationBundle[] = [
|
const translations: TranslationBundle[] = [
|
||||||
{locale: 'fr', translations: {}},
|
{locale: 'fr', translations: {}},
|
||||||
];
|
];
|
||||||
|
@ -104,24 +114,20 @@ describe('SourceFileTranslationHandler', () => {
|
||||||
`const x="${locale}";const y="${locale}";const z="${locale}"||"default";`;
|
`const x="${locale}";const y="${locale}";const z="${locale}"||"default";`;
|
||||||
|
|
||||||
handler.translate(
|
handler.translate(
|
||||||
diagnostics, '/root/path', 'relative/path.js', contents, mockOutputPathFn, translations,
|
diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations, 'en-US');
|
||||||
'en-US');
|
|
||||||
|
|
||||||
expect(FileUtils.writeFile)
|
expect(fs.readFile(frTranslationPath)).toEqual(getOutput('fr'));
|
||||||
.toHaveBeenCalledWith('/translations/fr/relative/path.js', getOutput('fr'));
|
expect(fs.readFile(enUSTranslationPath)).toEqual(getOutput('en-US'));
|
||||||
expect(FileUtils.writeFile)
|
|
||||||
.toHaveBeenCalledWith('/translations/en-US/relative/path.js', getOutput('en-US'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if the file is not valid JS', () => {
|
it('should error if the file is not valid JS', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new SourceFileTranslationHandler();
|
const handler = new SourceFileTranslationHandler(fs);
|
||||||
const translations = [{locale: 'en', translations: {}}];
|
const translations = [{locale: 'en', translations: {}}];
|
||||||
const contents = Buffer.from('this is not a valid $localize file.');
|
const contents = Buffer.from('this is not a valid $localize file.');
|
||||||
expect(
|
expect(
|
||||||
() => handler.translate(
|
() => handler.translate(
|
||||||
diagnostics, '/root/path', 'relative/path.js', contents, mockOutputPathFn,
|
diagnostics, rootPath, filePath, contents, mockOutputPathFn, translations))
|
||||||
translations))
|
|
||||||
.toThrowError();
|
.toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -130,3 +136,4 @@ describe('SourceFileTranslationHandler', () => {
|
||||||
function mockOutputPathFn(locale: string, relativePath: string) {
|
function mockOutputPathFn(locale: string, relativePath: string) {
|
||||||
return `/translations/${locale}/${relativePath}`;
|
return `/translations/${locale}/${relativePath}`;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -5,32 +5,52 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {ɵParsedTranslation, ɵparseTranslation} from '@angular/localize';
|
||||||
|
|
||||||
import {DiagnosticHandlingStrategy, Diagnostics} from '../../../src/diagnostics';
|
import {DiagnosticHandlingStrategy, Diagnostics} from '../../../src/diagnostics';
|
||||||
import {FileUtils} from '../../../src/file_utils';
|
|
||||||
import {TranslationLoader} from '../../../src/translate/translation_files/translation_loader';
|
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';
|
import {TranslationParser} from '../../../src/translate/translation_files/translation_parsers/translation_parser';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
describe('TranslationLoader', () => {
|
describe('TranslationLoader', () => {
|
||||||
describe('loadBundles()', () => {
|
describe('loadBundles()', () => {
|
||||||
const alwaysCanParse = () => true;
|
const alwaysCanParse = () => true;
|
||||||
const neverCanParse = () => false;
|
const neverCanParse = () => false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(FileUtils, 'readFile').and.returnValues('english messages', 'french messages');
|
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 call `canParse()` and `parse()` for each file', () => {
|
it('should call `canParse()` and `parse()` for each file', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser = new MockTranslationParser(alwaysCanParse, 'fr');
|
const parser = new MockTranslationParser(alwaysCanParse, 'fr');
|
||||||
const loader = new TranslationLoader([parser], 'error', diagnostics);
|
const loader = new TranslationLoader(fs, [parser], 'error', diagnostics);
|
||||||
loader.loadBundles([['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []);
|
loader.loadBundles([[enTranslationPath], [frTranslationPath]], []);
|
||||||
expect(parser.log).toEqual([
|
expect(parser.log).toEqual([
|
||||||
'canParse(/src/locale/messages.en.xlf, english messages)',
|
`canParse(${enTranslationPath}, ${enTranslationContent})`,
|
||||||
'parse(/src/locale/messages.en.xlf, english messages)',
|
`parse(${enTranslationPath}, ${enTranslationContent})`,
|
||||||
'canParse(/src/locale/messages.fr.xlf, french messages)',
|
`canParse(${frTranslationPath}, ${frTranslationContent})`,
|
||||||
'parse(/src/locale/messages.fr.xlf, french messages)',
|
`parse(${frTranslationPath}, ${frTranslationContent})`,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -39,17 +59,17 @@ describe('TranslationLoader', () => {
|
||||||
const parser1 = new MockTranslationParser(neverCanParse);
|
const parser1 = new MockTranslationParser(neverCanParse);
|
||||||
const parser2 = new MockTranslationParser(alwaysCanParse, 'fr');
|
const parser2 = new MockTranslationParser(alwaysCanParse, 'fr');
|
||||||
const parser3 = new MockTranslationParser(alwaysCanParse, 'en');
|
const parser3 = new MockTranslationParser(alwaysCanParse, 'en');
|
||||||
const loader = new TranslationLoader([parser1, parser2, parser3], 'error', diagnostics);
|
const loader = new TranslationLoader(fs, [parser1, parser2, parser3], 'error', diagnostics);
|
||||||
loader.loadBundles([['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []);
|
loader.loadBundles([[enTranslationPath], [frTranslationPath]], []);
|
||||||
expect(parser1.log).toEqual([
|
expect(parser1.log).toEqual([
|
||||||
'canParse(/src/locale/messages.en.xlf, english messages)',
|
`canParse(${enTranslationPath}, ${enTranslationContent})`,
|
||||||
'canParse(/src/locale/messages.fr.xlf, french messages)',
|
`canParse(${frTranslationPath}, ${frTranslationContent})`,
|
||||||
]);
|
]);
|
||||||
expect(parser2.log).toEqual([
|
expect(parser2.log).toEqual([
|
||||||
'canParse(/src/locale/messages.en.xlf, english messages)',
|
`canParse(${enTranslationPath}, ${enTranslationContent})`,
|
||||||
'parse(/src/locale/messages.en.xlf, english messages)',
|
`parse(${enTranslationPath}, ${enTranslationContent})`,
|
||||||
'canParse(/src/locale/messages.fr.xlf, french messages)',
|
`canParse(${frTranslationPath}, ${frTranslationContent})`,
|
||||||
'parse(/src/locale/messages.fr.xlf, french messages)',
|
`parse(${frTranslationPath}, ${frTranslationContent})`,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,9 +77,8 @@ describe('TranslationLoader', () => {
|
||||||
const translations = {};
|
const translations = {};
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser = new MockTranslationParser(alwaysCanParse, 'pl', translations);
|
const parser = new MockTranslationParser(alwaysCanParse, 'pl', translations);
|
||||||
const loader = new TranslationLoader([parser], 'error', diagnostics);
|
const loader = new TranslationLoader(fs, [parser], 'error', diagnostics);
|
||||||
const result = loader.loadBundles(
|
const result = loader.loadBundles([[enTranslationPath], [frTranslationPath]], []);
|
||||||
[['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []);
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{locale: 'pl', translations, diagnostics: new Diagnostics()},
|
{locale: 'pl', translations, diagnostics: new Diagnostics()},
|
||||||
{locale: 'pl', translations, diagnostics: new Diagnostics()},
|
{locale: 'pl', translations, diagnostics: new Diagnostics()},
|
||||||
|
@ -70,9 +89,8 @@ describe('TranslationLoader', () => {
|
||||||
const translations = {};
|
const translations = {};
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser = new MockTranslationParser(alwaysCanParse, undefined, translations);
|
const parser = new MockTranslationParser(alwaysCanParse, undefined, translations);
|
||||||
const loader = new TranslationLoader([parser], 'error', diagnostics);
|
const loader = new TranslationLoader(fs, [parser], 'error', diagnostics);
|
||||||
const result = loader.loadBundles(
|
const result = loader.loadBundles([[enTranslationPath], [frTranslationPath]], ['en', 'fr']);
|
||||||
[['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], ['en', 'fr']);
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{locale: 'en', translations, diagnostics: new Diagnostics()},
|
{locale: 'en', translations, diagnostics: new Diagnostics()},
|
||||||
{locale: 'fr', translations, diagnostics: new Diagnostics()},
|
{locale: 'fr', translations, diagnostics: new Diagnostics()},
|
||||||
|
@ -81,13 +99,8 @@ describe('TranslationLoader', () => {
|
||||||
|
|
||||||
it('should merge multiple translation files, if given, for a each locale', () => {
|
it('should merge multiple translation files, if given, for a each locale', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser1 = new MockTranslationParser(
|
const loader = new TranslationLoader(fs, [jsonParser], 'error', diagnostics);
|
||||||
f => f.includes('messages.fr'), 'fr', {'a': ɵparseTranslation('A')});
|
const result = loader.loadBundles([[frTranslationPath, frExtraTranslationPath]], []);
|
||||||
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([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
locale: 'fr',
|
locale: 'fr',
|
||||||
|
@ -99,16 +112,15 @@ describe('TranslationLoader', () => {
|
||||||
|
|
||||||
const allDiagnosticModes: DiagnosticHandlingStrategy[] = ['ignore', 'warning', 'error'];
|
const allDiagnosticModes: DiagnosticHandlingStrategy[] = ['ignore', 'warning', 'error'];
|
||||||
allDiagnosticModes.forEach(
|
allDiagnosticModes.forEach(
|
||||||
mode => it(
|
mode =>
|
||||||
`should ${mode} on duplicate messages when merging multiple translation files`, () => {
|
it(`should ${mode} on duplicate messages when merging multiple translation files`,
|
||||||
|
() => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser1 = new MockTranslationParser(
|
const loader = new TranslationLoader(fs, [jsonParser], mode, diagnostics);
|
||||||
f => f.includes('messages.fr'), 'fr', {'a': ɵparseTranslation('A')});
|
// Change the fs-extra file to have the same translations as fr.
|
||||||
const parser2 = new MockTranslationParser(
|
fs.writeFile(frExtraTranslationPath, frTranslationContent);
|
||||||
f => f.includes('extra.fr'), 'fr', {'a': ɵparseTranslation('B')});
|
const result =
|
||||||
const loader = new TranslationLoader([parser1, parser2], mode, diagnostics);
|
loader.loadBundles([[frTranslationPath, frExtraTranslationPath]], []);
|
||||||
const result = loader.loadBundles(
|
|
||||||
[['/src/locale/messages.fr.xlf', '/src/locale/extra.fr.xlf']], []);
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
locale: 'fr',
|
locale: 'fr',
|
||||||
|
@ -120,45 +132,43 @@ describe('TranslationLoader', () => {
|
||||||
if (mode === 'error' || mode === 'warning') {
|
if (mode === 'error' || mode === 'warning') {
|
||||||
expect(diagnostics.messages).toEqual([{
|
expect(diagnostics.messages).toEqual([{
|
||||||
type: mode,
|
type: mode,
|
||||||
message:
|
message: `Duplicate translations for message "a" when merging "${
|
||||||
`Duplicate translations for message "a" when merging "/src/locale/extra.fr.xlf".`
|
frExtraTranslationPath}".`
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should warn if the provided locales do not match the parsed locales', () => {
|
it('should warn if the provided locales do not match the parsed locales', () => {
|
||||||
const translations = {};
|
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser = new MockTranslationParser(alwaysCanParse, 'pl', translations);
|
const loader = new TranslationLoader(fs, [jsonParser], 'error', diagnostics);
|
||||||
const loader = new TranslationLoader([parser], 'error', diagnostics);
|
loader.loadBundles([[enTranslationPath], [frTranslationPath]], [undefined, 'es']);
|
||||||
loader.loadBundles(
|
|
||||||
[['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], [undefined, 'FR']);
|
|
||||||
expect(diagnostics.messages.length).toEqual(1);
|
expect(diagnostics.messages.length).toEqual(1);
|
||||||
expect(diagnostics.messages)
|
expect(diagnostics.messages)
|
||||||
.toContain(
|
.toContain(
|
||||||
{
|
{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message:
|
message:
|
||||||
`The provided locale "FR" does not match the target locale "pl" found in the translation file "/src/locale/messages.fr.xlf".`,
|
`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', () => {
|
it('should warn on differing target locales when merging multiple translation files', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser1 = new MockTranslationParser(
|
|
||||||
f => f.includes('messages-1.fr'), 'fr', {'a': ɵparseTranslation('A')});
|
const fr1 = absoluteFrom('/src/locale/messages-1.fr.json');
|
||||||
const parser2 = new MockTranslationParser(
|
fs.writeFile(fr1, '{"locale":"fr", "translations": {"a": "A"}}');
|
||||||
f => f.includes('messages-2.fr'), 'fr', {'b': ɵparseTranslation('B')});
|
|
||||||
const parser3 = new MockTranslationParser(
|
const fr2 = absoluteFrom('/src/locale/messages-2.fr.json');
|
||||||
f => f.includes('messages.de'), 'de', {'c': ɵparseTranslation('C')});
|
fs.writeFile(fr2, '{"locale":"fr", "translations": {"b": "B"}}');
|
||||||
const loader = new TranslationLoader([parser1, parser2, parser3], 'error', diagnostics);
|
|
||||||
const result = loader.loadBundles(
|
const de = absoluteFrom('/src/locale/messages.de.json');
|
||||||
[[
|
fs.writeFile(de, '{"locale":"de", "translations": {"c": "C"}}');
|
||||||
'/src/locale/messages-1.fr.xlf', '/src/locale/messages-2.fr.xlf',
|
|
||||||
'/src/locale/messages.de.xlf'
|
const loader = new TranslationLoader(fs, [jsonParser], 'error', diagnostics);
|
||||||
]],
|
|
||||||
[]);
|
const result = loader.loadBundles([[fr1, fr2, de]], []);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
locale: 'fr',
|
locale: 'fr',
|
||||||
|
@ -174,8 +184,8 @@ describe('TranslationLoader', () => {
|
||||||
expect(diagnostics.messages).toEqual([{
|
expect(diagnostics.messages).toEqual([{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message:
|
message:
|
||||||
`When merging multiple translation files, the target locale "de" found in "/src/locale/messages.de.xlf" ` +
|
`When merging multiple translation files, the target locale "de" found in "${de}" ` +
|
||||||
`does not match the target locale "fr" found in earlier files ["/src/locale/messages-1.fr.xlf", "/src/locale/messages-2.fr.xlf"].`
|
`does not match the target locale "fr" found in earlier files ["${fr1}", "${fr2}"].`
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -183,21 +193,19 @@ describe('TranslationLoader', () => {
|
||||||
const translations = {};
|
const translations = {};
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser = new MockTranslationParser(alwaysCanParse, undefined, translations);
|
const parser = new MockTranslationParser(alwaysCanParse, undefined, translations);
|
||||||
const loader = new TranslationLoader([parser], 'error', diagnostics);
|
const loader = new TranslationLoader(fs, [parser], 'error', diagnostics);
|
||||||
expect(() => loader.loadBundles([['/src/locale/messages.en.xlf']], []))
|
expect(() => loader.loadBundles([[enTranslationPath]], []))
|
||||||
.toThrowError(
|
.toThrowError(`The translation file "${
|
||||||
'The translation file "/src/locale/messages.en.xlf" does not contain a target locale and no explicit locale was provided for this 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', () => {
|
it('should error if none of the parsers can parse the file', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const parser = new MockTranslationParser(neverCanParse);
|
const parser = new MockTranslationParser(neverCanParse);
|
||||||
const loader = new TranslationLoader([parser], 'error', diagnostics);
|
const loader = new TranslationLoader(fs, [parser], 'error', diagnostics);
|
||||||
expect(
|
expect(() => loader.loadBundles([[enTranslationPath], [frTranslationPath]], []))
|
||||||
() => loader.loadBundles(
|
.toThrowError(`There is no "TranslationParser" that can parse this translation file: ${
|
||||||
[['/src/locale/messages.en.xlf'], ['/src/locale/messages.fr.xlf']], []))
|
enTranslationPath}.`);
|
||||||
.toThrowError(
|
|
||||||
'There is no "TranslationParser" that can parse this translation file: /src/locale/messages.en.xlf.');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -215,6 +223,11 @@ class MockTranslationParser implements TranslationParser {
|
||||||
|
|
||||||
parse(filePath: string, fileContents: string) {
|
parse(filePath: string, fileContents: string) {
|
||||||
this.log.push(`parse(${filePath}, ${fileContents})`);
|
this.log.push(`parse(${filePath}, ${fileContents})`);
|
||||||
return {locale: this._locale, translations: this._translations, diagnostics: new Diagnostics()};
|
return {
|
||||||
|
locale: this._locale,
|
||||||
|
translations: this._translations,
|
||||||
|
diagnostics: new Diagnostics()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -5,53 +5,68 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* 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 {Diagnostics as Diagnostics} from '../../src/diagnostics';
|
||||||
import {FileUtils} from '../../src/file_utils';
|
|
||||||
import {OutputPathFn} from '../../src/translate/output_path';
|
import {OutputPathFn} from '../../src/translate/output_path';
|
||||||
import {TranslationBundle, TranslationHandler, Translator} from '../../src/translate/translator';
|
import {TranslationBundle, TranslationHandler, Translator} from '../../src/translate/translator';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
describe('Translator', () => {
|
describe('Translator', () => {
|
||||||
describe('translateFiles()', () => {
|
let fs: FileSystem;
|
||||||
|
let distDirectory: AbsoluteFsPath;
|
||||||
|
let imgDirectory: AbsoluteFsPath;
|
||||||
|
let file1Path: PathSegment;
|
||||||
|
let imgPath: PathSegment;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(FileUtils, 'readFileBuffer')
|
fs = getFileSystem();
|
||||||
.and.returnValues(Buffer.from('resource file 1'), Buffer.from('resource file 2'));
|
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', () => {
|
describe('translateFiles()', () => {
|
||||||
const translator = new Translator([new MockTranslationHandler()], new Diagnostics());
|
it('should call FileSystem.readFileBuffer load the resource file contents', () => {
|
||||||
translator.translateFiles(
|
const translator = new Translator(fs, [new MockTranslationHandler()], new Diagnostics());
|
||||||
['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, []);
|
spyOn(fs, 'readFileBuffer').and.callThrough();
|
||||||
expect(FileUtils.readFileBuffer).toHaveBeenCalledWith('/dist/file1.js');
|
translator.translateFiles([file1Path, imgPath], distDirectory, mockOutputPathFn, []);
|
||||||
expect(FileUtils.readFileBuffer).toHaveBeenCalledWith('/dist/images/img.gif');
|
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', () => {
|
it('should call `canTranslate()` and `translate()` for each file', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new MockTranslationHandler(true);
|
const handler = new MockTranslationHandler(true);
|
||||||
const translator = new Translator([handler], diagnostics);
|
const translator = new Translator(fs, [handler], diagnostics);
|
||||||
translator.translateFiles(
|
translator.translateFiles([file1Path, imgPath], distDirectory, mockOutputPathFn, []);
|
||||||
['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, []);
|
|
||||||
|
|
||||||
expect(handler.log).toEqual([
|
expect(handler.log).toEqual([
|
||||||
'canTranslate(file1.js, resource file 1)',
|
'canTranslate(file1.js, resource file 1)',
|
||||||
'translate(/dist, file1.js, resource file 1, ...)',
|
`translate(${distDirectory}, file1.js, resource file 1, ...)`,
|
||||||
'canTranslate(images/img.gif, resource file 2)',
|
'canTranslate(images/img.gif, resource file 2)',
|
||||||
'translate(/dist, images/img.gif, resource file 2, ...)',
|
`translate(${distDirectory}, images/img.gif, resource file 2, ...)`,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass the sourceLocale through to `translate()` if provided', () => {
|
it('should pass the sourceLocale through to `translate()` if provided', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new MockTranslationHandler(true);
|
const handler = new MockTranslationHandler(true);
|
||||||
const translator = new Translator([handler], diagnostics);
|
const translator = new Translator(fs, [handler], diagnostics);
|
||||||
translator.translateFiles(
|
translator.translateFiles(
|
||||||
['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, [], 'en-US');
|
[file1Path, imgPath], distDirectory, mockOutputPathFn, [], 'en-US');
|
||||||
|
|
||||||
expect(handler.log).toEqual([
|
expect(handler.log).toEqual([
|
||||||
'canTranslate(file1.js, resource file 1)',
|
'canTranslate(file1.js, resource file 1)',
|
||||||
'translate(/dist, file1.js, resource file 1, ..., en-US)',
|
`translate(${distDirectory}, file1.js, resource file 1, ..., en-US)`,
|
||||||
'canTranslate(images/img.gif, resource file 2)',
|
'canTranslate(images/img.gif, resource file 2)',
|
||||||
'translate(/dist, images/img.gif, resource file 2, ..., en-US)',
|
`translate(${distDirectory}, images/img.gif, resource file 2, ..., en-US)`,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -60,9 +75,8 @@ describe('Translator', () => {
|
||||||
const handler1 = new MockTranslationHandler(false);
|
const handler1 = new MockTranslationHandler(false);
|
||||||
const handler2 = new MockTranslationHandler(true);
|
const handler2 = new MockTranslationHandler(true);
|
||||||
const handler3 = new MockTranslationHandler(true);
|
const handler3 = new MockTranslationHandler(true);
|
||||||
const translator = new Translator([handler1, handler2, handler3], diagnostics);
|
const translator = new Translator(fs, [handler1, handler2, handler3], diagnostics);
|
||||||
translator.translateFiles(
|
translator.translateFiles([file1Path, imgPath], distDirectory, mockOutputPathFn, []);
|
||||||
['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, []);
|
|
||||||
|
|
||||||
expect(handler1.log).toEqual([
|
expect(handler1.log).toEqual([
|
||||||
'canTranslate(file1.js, resource file 1)',
|
'canTranslate(file1.js, resource file 1)',
|
||||||
|
@ -70,23 +84,22 @@ describe('Translator', () => {
|
||||||
]);
|
]);
|
||||||
expect(handler2.log).toEqual([
|
expect(handler2.log).toEqual([
|
||||||
'canTranslate(file1.js, resource file 1)',
|
'canTranslate(file1.js, resource file 1)',
|
||||||
'translate(/dist, file1.js, resource file 1, ...)',
|
`translate(${distDirectory}, file1.js, resource file 1, ...)`,
|
||||||
'canTranslate(images/img.gif, resource file 2)',
|
'canTranslate(images/img.gif, resource file 2)',
|
||||||
'translate(/dist, 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', () => {
|
it('should error if none of the handlers can handle the file', () => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
const handler = new MockTranslationHandler(false);
|
const handler = new MockTranslationHandler(false);
|
||||||
const translator = new Translator([handler], diagnostics);
|
const translator = new Translator(fs, [handler], diagnostics);
|
||||||
|
|
||||||
translator.translateFiles(
|
translator.translateFiles([file1Path, imgPath], distDirectory, mockOutputPathFn, []);
|
||||||
['/dist/file1.js', '/dist/images/img.gif'], '/dist', mockOutputPathFn, []);
|
|
||||||
|
|
||||||
expect(diagnostics.messages).toEqual([
|
expect(diagnostics.messages).toEqual([
|
||||||
{type: 'error', message: 'Unable to handle resource file: /dist/file1.js'},
|
{type: 'error', message: `Unable to handle resource file: ${file1Path}`},
|
||||||
{type: 'error', message: 'Unable to handle resource file: /dist/images/img.gif'},
|
{type: 'error', message: `Unable to handle resource file: ${imgPath}`},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -113,3 +126,4 @@ class MockTranslationHandler implements TranslationHandler {
|
||||||
function mockOutputPathFn(locale: string, relativePath: string) {
|
function mockOutputPathFn(locale: string, relativePath: string) {
|
||||||
return `translations/${locale}/${relativePath}`;
|
return `translations/${locale}/${relativePath}`;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue