diff --git a/modules/@angular/compiler-cli/integrationtest/src/basic.html b/modules/@angular/compiler-cli/integrationtest/src/basic.html index 75cbeb7137..4d1aebaf95 100644 --- a/modules/@angular/compiler-cli/integrationtest/src/basic.html +++ b/modules/@angular/compiler-cli/integrationtest/src/basic.html @@ -1,4 +1,4 @@ -<div [attr.array]="[0]" [attr.map]="{a:1}">{{ctxProp}}</div> +<div [attr.array]="[0]" [attr.map]="{a:1}" title="translate me" i18n-title="meaning|desc">{{ctxProp}}</div> <form><input type="button" [(ngModel)]="ctxProp"/></form> <my-comp *ngIf="ctxBool"></my-comp> -<div *ngFor="let x of ctxArr" [attr.value]="x"></div> \ No newline at end of file +<div *ngFor="let x of ctxArr" [attr.value]="x"></div> diff --git a/modules/@angular/compiler-cli/integrationtest/test/i18n_spec.ts b/modules/@angular/compiler-cli/integrationtest/test/i18n_spec.ts new file mode 100644 index 0000000000..23e9c10841 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/test/i18n_spec.ts @@ -0,0 +1,26 @@ +// Only needed to satisfy the check in core/src/util/decorators.ts +// TODO(alexeagle): maybe remove that check? +require('reflect-metadata'); + +require('@angular/platform-server/src/parse5_adapter.js').Parse5DomAdapter.makeCurrent(); +require('zone.js/dist/zone-node.js'); +require('zone.js/dist/long-stack-trace-zone.js'); +let serializer = require('@angular/compiler/src/i18n/xmb_serializer.js'); + +import * as fs from 'fs'; +import * as path from 'path'; + +describe("template i18n extraction output", () => { + const outDir = ''; + + it("should extract i18n messages", () => { + const xmbOutput = path.join(outDir, 'messages.xmb'); + expect(fs.existsSync(xmbOutput)).toBeTruthy(); + const xmb = fs.readFileSync(xmbOutput, {encoding: 'utf-8'}); + const res = serializer.deserializeXmb(xmb); + const keys = Object.keys(res.messages); + expect(keys.length).toEqual(1); + expect(res.errors.length).toEqual(0); + expect(res.messages[keys[0]][0].value).toEqual('translate me'); + }); +}); diff --git a/modules/@angular/compiler-cli/package.json b/modules/@angular/compiler-cli/package.json index c85548334b..f942242171 100644 --- a/modules/@angular/compiler-cli/package.json +++ b/modules/@angular/compiler-cli/package.json @@ -5,7 +5,8 @@ "main": "index.js", "typings": "index.d.ts", "bin": { - "ngc": "./src/main.js" + "ngc": "./src/main.js", + "ng-xi18n": "./src/extract_i18n.js" }, "dependencies": { "@angular/tsc-wrapped": "^0.1.0", diff --git a/modules/@angular/compiler-cli/src/compiler_private.ts b/modules/@angular/compiler-cli/src/compiler_private.ts index 7cea60bb00..1a82470bb1 100644 --- a/modules/@angular/compiler-cli/src/compiler_private.ts +++ b/modules/@angular/compiler-cli/src/compiler_private.ts @@ -1,10 +1,10 @@ import {__compiler_private__ as _c} from '@angular/compiler'; -export var AssetUrl: typeof _c.AssetUrl = _c.AssetUrl; export type AssetUrl = _c.AssetUrl; +export var AssetUrl: typeof _c.AssetUrl = _c.AssetUrl; -export var ImportGenerator: typeof _c.ImportGenerator = _c.ImportGenerator; export type ImportGenerator = _c.ImportGenerator; +export var ImportGenerator: typeof _c.ImportGenerator = _c.ImportGenerator; export type CompileMetadataResolver = _c.CompileMetadataResolver; export var CompileMetadataResolver: typeof _c.CompileMetadataResolver = _c.CompileMetadataResolver; @@ -12,6 +12,25 @@ export var CompileMetadataResolver: typeof _c.CompileMetadataResolver = _c.Compi export type HtmlParser = _c.HtmlParser; export var HtmlParser: typeof _c.HtmlParser = _c.HtmlParser; +export type I18nHtmlParser = _c.I18nHtmlParser; +export var I18nHtmlParser: typeof _c.I18nHtmlParser = _c.I18nHtmlParser; + +export type MessageExtractor = _c.MessageExtractor; +export var MessageExtractor: typeof _c.MessageExtractor = _c.MessageExtractor; + +export type ExtractionResult = _c.ExtractionResult; +export var ExtractionResult: typeof _c.ExtractionResult = _c.ExtractionResult; + +export type Message = _c.Message; +export var Message: typeof _c.Message = _c.Message; + +export var removeDuplicates: typeof _c.removeDuplicates = _c.removeDuplicates; +export var serializeXmb: typeof _c.serializeXmb = _c.serializeXmb; +export var deserializeXmb: typeof _c.deserializeXmb = _c.deserializeXmb; + +export type ParseError = _c.ParseError; +export var ParseError: typeof _c.ParseError = _c.ParseError; + export type DirectiveNormalizer = _c.DirectiveNormalizer; export var DirectiveNormalizer: typeof _c.DirectiveNormalizer = _c.DirectiveNormalizer; diff --git a/modules/@angular/compiler-cli/src/extract_i18n.ts b/modules/@angular/compiler-cli/src/extract_i18n.ts new file mode 100644 index 0000000000..4cdf9a1084 --- /dev/null +++ b/modules/@angular/compiler-cli/src/extract_i18n.ts @@ -0,0 +1,190 @@ +#!/usr/bin/env node + +/** + * Extract i18n messages from source code + */ + +// Must be imported first, because angular2 decorators throws on load. +import 'reflect-metadata'; + +import * as ts from 'typescript'; +import * as tsc from '@angular/tsc-wrapped'; +import * as path from 'path'; +import * as compiler from '@angular/compiler'; + +import {StaticReflector} from './static_reflector'; +import { + CompileMetadataResolver, + HtmlParser, + DirectiveNormalizer, + Lexer, + Parser, + TemplateParser, + DomElementSchemaRegistry, + StyleCompiler, + ViewCompiler, + TypeScriptEmitter, + MessageExtractor, + removeDuplicates, + ExtractionResult, + Message, + ParseError, + serializeXmb, +} from './compiler_private'; + +import {Parse5DomAdapter} from '@angular/platform-server'; + +import {NodeReflectorHost} from './reflector_host'; +import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities'; + + +function extract(ngOptions: tsc.AngularCompilerOptions, program: ts.Program, host: ts.CompilerHost) { + return Extractor.create(ngOptions, program, host).extract(); +} + +const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/; + +class Extractor { + constructor(private options: tsc.AngularCompilerOptions, + private program: ts.Program, public host: ts.CompilerHost, + private staticReflector: StaticReflector, private resolver: CompileMetadataResolver, + private compiler: compiler.OfflineCompiler, + private reflectorHost: NodeReflectorHost, private _extractor: MessageExtractor) {} + + private extractCmpMessages(metadatas: compiler.CompileDirectiveMetadata[]): Promise<ExtractionResult> { + if (!metadatas || !metadatas.length) { + return null; + } + + const normalize = (metadata: compiler.CompileDirectiveMetadata) => { + const directiveType = metadata.type.runtime; + const directives = this.resolver.getViewDirectivesMetadata(directiveType); + return Promise.all(directives.map(d => this.compiler.normalizeDirectiveMetadata(d))) + .then(normalizedDirectives => { + const pipes = this.resolver.getViewPipesMetadata(directiveType); + return new compiler.NormalizedComponentWithViewDirectives(metadata, + normalizedDirectives, pipes); + }); + }; + + return Promise + .all(metadatas.map(normalize)) + .then((cmps: compiler.NormalizedComponentWithViewDirectives[]) => { + let messages: Message[] = []; + let errors: ParseError[] = []; + cmps.forEach(cmp => { + // TODO(vicb): url + let result = this._extractor.extract(cmp.component.template.template, "url"); + errors = errors.concat(result.errors); + messages = messages.concat(result.messages); + }); + + // Extraction Result might contain duplicate messages at this point + return new ExtractionResult(messages, errors); + }); + } + + private readComponents(absSourcePath: string) { + const result: Promise<compiler.CompileDirectiveMetadata>[] = []; + const metadata = this.staticReflector.getModuleMetadata(absSourcePath); + if (!metadata) { + console.log(`WARNING: no metadata found for ${absSourcePath}`); + return result; + } + + const symbols = Object.keys(metadata['metadata']); + if (!symbols || !symbols.length) { + return result; + } + for (const symbol of symbols) { + const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath); + let directive: compiler.CompileDirectiveMetadata; + directive = this.resolver.maybeGetDirectiveMetadata(<any>staticType); + + if (!directive || !directive.isComponent) { + continue; + } + result.push(this.compiler.normalizeDirectiveMetadata(directive)); + } + return result; + } + + extract(): Promise<any> { + Parse5DomAdapter.makeCurrent(); + + const promises = this.program.getSourceFiles() + .map(sf => sf.fileName) + .filter(f => !GENERATED_FILES.test(f)) + .map((absSourcePath:string): Promise<any> => + Promise + .all(this.readComponents(absSourcePath)) + .then(metadatas => this.extractCmpMessages(metadatas)) + .catch(e => console.error(e.stack)) + ); + + let messages: Message[] = []; + let errors: ParseError[] = []; + + return Promise.all(promises) + .then(extractionResults => { + extractionResults + .filter(result => !!result) + .forEach(result => { + messages = messages.concat(result.messages); + errors = errors.concat(result.errors); + }); + + if (errors.length) { + throw errors; + } + + messages = removeDuplicates(messages); + + let genPath = path.join(this.options.genDir, 'messages.xmb'); + let msgBundle = serializeXmb(messages); + + this.host.writeFile(genPath, msgBundle, false); + }); + } + + static create(options: tsc.AngularCompilerOptions, program: ts.Program, + compilerHost: ts.CompilerHost): Extractor { + const xhr: compiler.XHR = {get: (s: string) => Promise.resolve(compilerHost.readFile(s))}; + const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver(); + const reflectorHost = new NodeReflectorHost(program, compilerHost, options); + const staticReflector = new StaticReflector(reflectorHost); + StaticAndDynamicReflectionCapabilities.install(staticReflector); + const htmlParser = new HtmlParser(); + const config = new compiler.CompilerConfig(true, true, true); + const normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser, config); + const parser = new Parser(new Lexer()); + const tmplParser = new TemplateParser(parser, new DomElementSchemaRegistry(), htmlParser, + /*console*/ null, []); + const offlineCompiler = new compiler.OfflineCompiler( + normalizer, tmplParser, new StyleCompiler(urlResolver), + new ViewCompiler(config), + new TypeScriptEmitter(reflectorHost), xhr); + const resolver = new CompileMetadataResolver( + new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector), + new compiler.ViewResolver(staticReflector), null, null, staticReflector); + + // TODO(vicb): handle implicit + const extractor = new MessageExtractor(htmlParser, parser, [], {}); + + return new Extractor(options, program, compilerHost, staticReflector, resolver, + offlineCompiler, reflectorHost, extractor); + } +} + +// Entry point +if (require.main === module) { + const args = require('minimist')(process.argv.slice(2)); + tsc.main(args.p || args.project || '.', args.basePath, extract) + .then(exitCode => process.exit(exitCode)) + .catch(e => { + console.error(e.stack); + console.error("Compilation failed"); + process.exit(1); + }); +} + diff --git a/modules/@angular/compiler-cli/tsconfig-es5.json b/modules/@angular/compiler-cli/tsconfig-es5.json index cb01ddface..4a0ff6f020 100644 --- a/modules/@angular/compiler-cli/tsconfig-es5.json +++ b/modules/@angular/compiler-cli/tsconfig-es5.json @@ -27,6 +27,7 @@ "files": [ "index.ts", "src/main.ts", + "src/extract_i18n.ts", "../../../node_modules/@types/node/index.d.ts", "../../../node_modules/@types/jasmine/index.d.ts", "../../../node_modules/zone.js/dist/zone.js.d.ts" diff --git a/modules/@angular/compiler/private_export.ts b/modules/@angular/compiler/private_export.ts index adb5074c37..6b5d414625 100644 --- a/modules/@angular/compiler/private_export.ts +++ b/modules/@angular/compiler/private_export.ts @@ -2,15 +2,19 @@ import * as selector from './src/selector'; import * as path_util from './src/output/path_util'; import * as metadata_resolver from './src/metadata_resolver'; import * as html_parser from './src/html_parser'; +import * as i18n_html_parser from './src/i18n/i18n_html_parser'; import * as directive_normalizer from './src/directive_normalizer'; import * as lexer from './src/expression_parser/lexer'; -import * as parse_util from './src/parse_util'; import * as parser from './src/expression_parser/parser'; import * as template_parser from './src/template_parser'; import * as dom_element_schema_registry from './src/schema/dom_element_schema_registry'; import * as style_compiler from './src/style_compiler'; import * as view_compiler from './src/view_compiler/view_compiler'; import * as ts_emitter from './src/output/ts_emitter'; +import * as i18n_extractor from './src/i18n/message_extractor'; +import * as i18n_message from './src/i18n/message'; +import * as xmb_serializer from './src/i18n/xmb_serializer'; +import * as parse_util from './src/parse_util'; export namespace __compiler_private__ { export type SelectorMatcher = selector.SelectorMatcher; @@ -31,6 +35,23 @@ export namespace __compiler_private__ { export type HtmlParser = html_parser.HtmlParser; export var HtmlParser = html_parser.HtmlParser; + export type I18nHtmlParser = i18n_html_parser.I18nHtmlParser; + export var I18nHtmlParser = i18n_html_parser.I18nHtmlParser; + + export type ExtractionResult = i18n_extractor.ExtractionResult; + export var ExtractionResult = i18n_extractor.ExtractionResult; + + export type Message = i18n_message.Message; + export var Message = i18n_message.Message; + + export type MessageExtractor = i18n_extractor.MessageExtractor; + export var MessageExtractor = i18n_extractor.MessageExtractor; + + export var removeDuplicates = i18n_extractor.removeDuplicates; + + export var serializeXmb = xmb_serializer.serializeXmb; + export var deserializeXmb = xmb_serializer.deserializeXmb; + export type DirectiveNormalizer = directive_normalizer.DirectiveNormalizer; export var DirectiveNormalizer = directive_normalizer.DirectiveNormalizer; diff --git a/modules/@angular/http/http.ts b/modules/@angular/http/http.ts index f9e9f46858..37e3831f3e 100644 --- a/modules/@angular/http/http.ts +++ b/modules/@angular/http/http.ts @@ -190,7 +190,7 @@ export const HTTP_PROVIDERS: any[] = [ {provide: XSRFStrategy, useValue: new CookieXSRFStrategy()}, ]; -function httpFactory(xhrBackend: XHRBackend, requestOptions: RequestOptions): Http { +export function httpFactory(xhrBackend: XHRBackend, requestOptions: RequestOptions): Http { return new Http(xhrBackend, requestOptions); } diff --git a/modules/@angular/router/src/router_providers_common.ts b/modules/@angular/router/src/router_providers_common.ts index a25a53b42a..d90c56ddb4 100644 --- a/modules/@angular/router/src/router_providers_common.ts +++ b/modules/@angular/router/src/router_providers_common.ts @@ -15,24 +15,24 @@ export const ROUTER_PROVIDERS_COMMON: any[] = /*@ts2dart_const*/[ provide: Router, useFactory: routerFactory, deps: /*@ts2dart_const*/ - [ApplicationRef, ComponentResolver, RouterUrlSerializer, RouterOutletMap, Location], + [ApplicationRef, ComponentResolver, RouterUrlSerializer, RouterOutletMap, Location], }, /*@ts2dart_Provider*/ {provide: RouteSegment, useFactory: routeSegmentFactory, deps: [Router]} ]; -function routerFactory(app: ApplicationRef, componentResolver: ComponentResolver, - urlSerializer: RouterUrlSerializer, routerOutletMap: RouterOutletMap, - location: Location): Router { +export function routerFactory(app: ApplicationRef, componentResolver: ComponentResolver, + urlSerializer: RouterUrlSerializer, routerOutletMap: RouterOutletMap, + location: Location): Router { if (app.componentTypes.length == 0) { throw new BaseException("Bootstrap at least one component before injecting Router."); } // TODO: vsavkin this should not be null let router = new Router(null, app.componentTypes[0], componentResolver, urlSerializer, - routerOutletMap, location); + routerOutletMap, location); app.registerDisposeListener(() => router.dispose()); return router; } -function routeSegmentFactory(router: Router): RouteSegment { +export function routeSegmentFactory(router: Router): RouteSegment { return router.routeTree.root; } diff --git a/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts b/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts index 3d7833642b..180309efb7 100644 --- a/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts +++ b/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts @@ -1,5 +1,4 @@ import { - provide, ChangeDetectorRef, Injector, OnChanges, @@ -7,7 +6,8 @@ import { ComponentRef, SimpleChange, SimpleChanges, - ReflectiveInjector + ReflectiveInjector, + EventEmitter } from '@angular/core'; import {NG1_SCOPE} from './constants'; import {ComponentInfo} from './metadata'; @@ -145,11 +145,11 @@ export class DowngradeNg2ComponentAdapter { if (assignExpr && !setter) { throw new Error(`Expression '${expr}' is not assignable!`); } - var emitter = this.component[output.prop]; + var emitter = this.component[output.prop] as EventEmitter<any>; if (emitter) { emitter.subscribe({ - next: assignExpr ? ((setter) => (value) => setter(this.scope, value))(setter) : - ((getter) => (value) => getter(this.scope, {$event: value}))(getter) + next: assignExpr ? ((setter: any) => v => setter(this.scope, v))(setter) : + ((getter: any) => v => getter(this.scope, {$event: v}))(getter) }); } else { throw new Error(`Missing emitter '${output.prop}' on component '${this.info.selector}'!`); diff --git a/modules/playground/src/bootstrap.ts b/modules/playground/src/bootstrap.ts index bcc91ef2a8..03460b3b4f 100644 --- a/modules/playground/src/bootstrap.ts +++ b/modules/playground/src/bootstrap.ts @@ -9,7 +9,7 @@ declare var System: any; writeScriptTag('/all/playground/vendor/system.src.js'); writeScriptTag('/all/playground/vendor/Reflect.js'); writeScriptTag('/all/playground/vendor/rxjs/bundles/Rx.js', 'playgroundBootstrap()'); - global.playgroundBootstrap = playgroundBootstrap; + (<any>global).playgroundBootstrap = playgroundBootstrap; function playgroundBootstrap() { // check query param diff --git a/scripts/ci-lite/offline_compiler_test.sh b/scripts/ci-lite/offline_compiler_test.sh index e8ef3d1c76..666da5fa8b 100755 --- a/scripts/ci-lite/offline_compiler_test.sh +++ b/scripts/ci-lite/offline_compiler_test.sh @@ -32,6 +32,7 @@ cp -v package.json $TMP # Compile the compiler-cli integration tests ./node_modules/.bin/ngc + ./node_modules/.bin/ng-xi18n ./node_modules/.bin/jasmine init # Run compiler-cli integration tests in node