1
0
mirror of https://github.com/angular/angular-cli.git synced 2025-05-19 04:26:01 +08:00

feat(@angular-devkit/build-angular): implement stable architect API for extract-i18n

This commit is contained in:
Charles Lyding 2019-03-19 14:07:26 -04:00 committed by Hans
parent c840fcb529
commit a011863477
4 changed files with 168 additions and 59 deletions
packages/angular_devkit/build_angular

@ -21,6 +21,7 @@
},
"extract-i18n": {
"class": "./src/extract-i18n",
"implementation": "./src/extract-i18n/index2",
"schema": "./src/extract-i18n/schema.json",
"description": "Extract i18n strings from a browser app."
},

@ -0,0 +1,93 @@
/**
* @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 {
BuilderContext,
createBuilder,
targetFromTargetString,
} from '@angular-devkit/architect/src/index2';
import { runWebpack } from '@angular-devkit/build-webpack/src/webpack/index2';
import { JsonObject } from '@angular-devkit/core';
import * as path from 'path';
import * as webpack from 'webpack';
import {
getAotConfig,
getCommonConfig,
getStatsConfig,
getStylesConfig,
} from '../angular-cli-files/models/webpack-configs';
import { Schema as BrowserBuilderOptions } from '../browser/schema';
import { generateBrowserWebpackConfigFromContext } from '../utils/webpack-browser-config';
import { Schema as ExtractI18nBuilderOptions } from './schema';
function getI18nOutfile(format: string | undefined) {
switch (format) {
case 'xmb':
return 'messages.xmb';
case 'xlf':
case 'xlif':
case 'xliff':
case 'xlf2':
case 'xliff2':
return 'messages.xlf';
default:
throw new Error(`Unsupported format "${format}"`);
}
}
class InMemoryOutputPlugin {
apply(compiler: webpack.Compiler): void {
// tslint:disable-next-line:no-any
compiler.outputFileSystem = new (webpack as any).MemoryOutputFileSystem();
}
}
export async function execute(options: ExtractI18nBuilderOptions, context: BuilderContext) {
const browserTarget = targetFromTargetString(options.browserTarget);
const browserOptions = await context.validateOptions<JsonObject & BrowserBuilderOptions>(
await context.getTargetOptions(browserTarget),
await context.getBuilderNameForTarget(browserTarget),
);
// We need to determine the outFile name so that AngularCompiler can retrieve it.
let outFile = options.outFile || getI18nOutfile(options.i18nFormat);
if (options.outputPath) {
// AngularCompilerPlugin doesn't support genDir so we have to adjust outFile instead.
outFile = path.join(options.outputPath, outFile);
}
const { config } = await generateBrowserWebpackConfigFromContext(
{
...browserOptions,
optimization: {
scripts: false,
styles: false,
},
i18nLocale: options.i18nLocale,
i18nFormat: options.i18nFormat,
i18nFile: outFile,
aot: true,
progress: options.progress,
assets: [],
scripts: [],
styles: [],
deleteOutputPath: false,
},
context,
wco => [
{ plugins: [new InMemoryOutputPlugin()] },
getCommonConfig(wco),
getAotConfig(wco, true),
getStylesConfig(wco),
getStatsConfig(wco),
],
);
return runWebpack(config, context).toPromise();
}
export default createBuilder<JsonObject & ExtractI18nBuilderOptions>(execute);

@ -1,4 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Extract i18n Target",
"description": "Extract i18n target options for Build Facade.",
"type": "object",

@ -5,105 +5,119 @@
* 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 { DefaultTimeout, TestLogger, runTargetSpec } from '@angular-devkit/architect/testing';
import { Architect } from '@angular-devkit/architect/src/index2';
import { TestLogger } from '@angular-devkit/architect/testing';
import { join, normalize, virtualFs } from '@angular-devkit/core';
import { tap } from 'rxjs/operators';
import { extractI18nTargetSpec, host } from '../utils';
import { createArchitect, extractI18nTargetSpec, host } from '../utils';
describe('Extract i18n Target', () => {
const extractionFile = join(normalize('src'), 'messages.xlf');
let architect: Architect;
beforeEach(done => host.initialize().toPromise().then(done, done.fail));
afterEach(done => host.restore().toPromise().then(done, done.fail));
beforeEach(async () => {
await host.initialize().toPromise();
architect = (await createArchitect(host.root())).architect;
});
it('works', (done) => {
afterEach(() => host.restore().toPromise());
it('works', async () => {
host.appendToFile('src/app/app.component.html', '<p i18n>i18n test</p>');
runTargetSpec(host, extractI18nTargetSpec).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
expect(host.scopedSync().exists((extractionFile))).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toMatch(/i18n test/);
}),
).toPromise().then(done, done.fail);
const run = await architect.scheduleTarget(extractI18nTargetSpec);
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
await run.stop();
const exists = host.scopedSync().exists(extractionFile);
expect(exists).toBe(true);
if (exists) {
const content = virtualFs.fileBufferToString(host.scopedSync().read(extractionFile));
expect(content).toContain('i18n test');
}
}, 30000);
it('shows errors', (done) => {
it('shows errors', async () => {
const logger = new TestLogger('i18n-errors');
host.appendToFile('src/app/app.component.html',
'<p i18n>Hello world <span i18n>inner</span></p>');
runTargetSpec(host, extractI18nTargetSpec, {}, DefaultTimeout, logger).pipe(
tap((buildEvent) => {
expect(buildEvent.success).toBe(false);
const msg = 'Could not mark an element as translatable inside a translatable section';
expect(logger.includes(msg)).toBe(true);
}),
).toPromise().then(done, done.fail);
const run = await architect.scheduleTarget(extractI18nTargetSpec, undefined, { logger });
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: false }));
await run.stop();
const msg = 'Could not mark an element as translatable inside a translatable section';
expect(logger.includes(msg)).toBe(true);
}, 30000);
it('supports locale', (done) => {
it('supports locale', async () => {
host.appendToFile('src/app/app.component.html', '<p i18n>i18n test</p>');
const overrides = { i18nLocale: 'fr' };
runTargetSpec(host, extractI18nTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
expect(host.scopedSync().exists((extractionFile))).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toContain('source-language="fr"');
}),
).toPromise().then(done, done.fail);
const run = await architect.scheduleTarget(extractI18nTargetSpec, overrides);
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
await run.stop();
expect(host.scopedSync().exists((extractionFile))).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toContain('source-language="fr"');
}, 30000);
it('supports out file', (done) => {
it('supports out file', async () => {
host.appendToFile('src/app/app.component.html', '<p i18n>i18n test</p>');
const outFile = 'messages.fr.xlf';
const extractionFile = join(normalize('src'), outFile);
const overrides = { outFile };
runTargetSpec(host, extractI18nTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
expect(host.scopedSync().exists(extractionFile)).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toMatch(/i18n test/);
}),
).toPromise().then(done, done.fail);
const run = await architect.scheduleTarget(extractI18nTargetSpec, overrides);
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
await run.stop();
expect(host.scopedSync().exists(extractionFile)).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toMatch(/i18n test/);
}, 30000);
it('supports output path', (done) => {
it('supports output path', async () => {
host.appendToFile('src/app/app.component.html', '<p i18n>i18n test</p>');
// Note: this folder will not be created automatically. It must exist beforehand.
const outputPath = 'app';
const extractionFile = join(normalize('src'), outputPath, 'messages.xlf');
const overrides = { outputPath };
runTargetSpec(host, extractI18nTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
expect(host.scopedSync().exists(extractionFile)).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toMatch(/i18n test/);
}),
).toPromise().then(done, done.fail);
const run = await architect.scheduleTarget(extractI18nTargetSpec, overrides);
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
await run.stop();
expect(host.scopedSync().exists(extractionFile)).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toMatch(/i18n test/);
}, 30000);
it('supports i18n format', (done) => {
it('supports i18n format', async () => {
host.appendToFile('src/app/app.component.html', '<p i18n>i18n test</p>');
const extractionFile = join(normalize('src'), 'messages.xmb');
const overrides = { i18nFormat: 'xmb' };
runTargetSpec(host, extractI18nTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
expect(host.scopedSync().exists(extractionFile)).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toMatch(/i18n test/);
}),
).toPromise().then(done, done.fail);
const run = await architect.scheduleTarget(extractI18nTargetSpec, overrides);
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
await run.stop();
expect(host.scopedSync().exists(extractionFile)).toBe(true);
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toMatch(/i18n test/);
}, 30000);
});