diff --git a/modules/angular2/i18n.ts b/modules/angular2/i18n.ts new file mode 100644 index 000000000000..6f22fe3773ee --- /dev/null +++ b/modules/angular2/i18n.ts @@ -0,0 +1,8 @@ +/** + * @module + * @description + * Entry point to i18n + */ +export * from './src/i18n/message'; +export * from './src/i18n/xmb_serializer'; +export * from './src/i18n/message_extractor'; \ No newline at end of file diff --git a/modules/angular2/pubspec.yaml b/modules/angular2/pubspec.yaml index 82266fc98a4a..8dbd0125d15f 100644 --- a/modules/angular2/pubspec.yaml +++ b/modules/angular2/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: protobuf: '^0.5.0' source_span: '^1.0.0' stack_trace: '^1.1.1' + build: '>=0.0.1' dev_dependencies: transformer_test: '^0.2.0' guinness: '^0.1.18' diff --git a/modules/angular2/src/facade/lang.dart b/modules/angular2/src/facade/lang.dart index 2822c2743319..23585a446ab1 100644 --- a/modules/angular2/src/facade/lang.dart +++ b/modules/angular2/src/facade/lang.dart @@ -1,6 +1,6 @@ library angular.core.facade.lang; -export 'dart:core' show Type, RegExp, print, DateTime; +export 'dart:core' show Type, RegExp, print, DateTime, Uri; import 'dart:math' as math; import 'dart:convert' as convert; import 'dart:async' show Future; @@ -372,3 +372,7 @@ num bitWiseAnd(List values) { var val = values.reduce((num a, num b) => (a as int) & (b as int)); return val as num; } + +String escape(String s) { + return Uri.encodeComponent(s); +} \ No newline at end of file diff --git a/modules/angular2/src/facade/lang.ts b/modules/angular2/src/facade/lang.ts index 8d409f7776d5..a977d34ab8c1 100644 --- a/modules/angular2/src/facade/lang.ts +++ b/modules/angular2/src/facade/lang.ts @@ -30,6 +30,7 @@ export interface BrowserNodeGlobal { clearTimeout: Function; setInterval: Function; clearInterval: Function; + encodeURI: Function; } // TODO(jteplitz602): Load WorkerGlobalScope from lib.webworker.d.ts file #3492 @@ -481,3 +482,7 @@ export function bitWiseOr(values: number[]): number { export function bitWiseAnd(values: number[]): number { return values.reduce((a, b) => { return a & b; }); } + +export function escape(s: string): string { + return _global.encodeURI(s); +} \ No newline at end of file diff --git a/modules/angular2/src/i18n/message.ts b/modules/angular2/src/i18n/message.ts new file mode 100644 index 000000000000..2ad922d2ffde --- /dev/null +++ b/modules/angular2/src/i18n/message.ts @@ -0,0 +1,21 @@ +import {isPresent, escape} from 'angular2/src/facade/lang'; + +/** + * A message extracted from a template. + * + * The identity of a message is comprised of `content` and `meaning`. + * + * `description` is additional information provided to the translator. + */ +export class Message { + constructor(public content: string, public meaning: string, public description: string) {} +} + +/** + * Computes the id of a message + */ +export function id(m: Message): string { + let meaning = isPresent(m.meaning) ? m.meaning : ""; + let content = isPresent(m.content) ? m.content : ""; + return escape(`$ng|${meaning}|${content}`); +} \ No newline at end of file diff --git a/modules/angular2/src/i18n/message_extractor.ts b/modules/angular2/src/i18n/message_extractor.ts index 9fdf0d2b7eb3..21fabe9592e3 100644 --- a/modules/angular2/src/i18n/message_extractor.ts +++ b/modules/angular2/src/i18n/message_extractor.ts @@ -13,21 +13,11 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang'; import {StringMapWrapper} from 'angular2/src/facade/collection'; import {Parser} from 'angular2/src/core/change_detection/parser/parser'; import {Interpolation} from 'angular2/src/core/change_detection/parser/ast'; +import {Message, id} from './message'; const I18N_ATTR = "i18n"; const I18N_ATTR_PREFIX = "i18n-"; -/** - * A message extracted from a template. - * - * The identity of a message is comprised of `content` and `meaning`. - * - * `description` is additional information provided to the translator. - */ -export class Message { - constructor(public content: string, public meaning: string, public description: string) {} -} - /** * All messages extracted from a template. */ @@ -56,9 +46,8 @@ export class I18nExtractionError extends ParseError { export function removeDuplicates(messages: Message[]): Message[] { let uniq: {[key: string]: Message} = {}; messages.forEach(m => { - let key = `$ng__${m.meaning}__|${m.content}`; - if (!StringMapWrapper.contains(uniq, key)) { - uniq[key] = m; + if (!StringMapWrapper.contains(uniq, id(m))) { + uniq[id(m)] = m; } }); return StringMapWrapper.values(uniq); diff --git a/modules/angular2/src/i18n/xmb_serializer.ts b/modules/angular2/src/i18n/xmb_serializer.ts new file mode 100644 index 000000000000..f39cefb3a69e --- /dev/null +++ b/modules/angular2/src/i18n/xmb_serializer.ts @@ -0,0 +1,12 @@ +import {isPresent} from 'angular2/src/facade/lang'; +import {Message, id} from './message'; + +export function serialize(messages: Message[]): string { + let ms = messages.map((m) => _serializeMessage(m)).join(""); + return `${ms}`; +} + +function _serializeMessage(m: Message): string { + let desc = isPresent(m.description) ? ` desc='${m.description}'` : ""; + return `${m.content}`; +} \ No newline at end of file diff --git a/modules/angular2/test/i18n/message_extractor_spec.ts b/modules/angular2/test/i18n/message_extractor_spec.ts index ab15741ce0c9..7b1d1a8b3bd1 100644 --- a/modules/angular2/test/i18n/message_extractor_spec.ts +++ b/modules/angular2/test/i18n/message_extractor_spec.ts @@ -12,7 +12,8 @@ import { } from 'angular2/testing_internal'; import {HtmlParser} from 'angular2/src/compiler/html_parser'; -import {MessageExtractor, Message, removeDuplicates} from 'angular2/src/i18n/message_extractor'; +import {MessageExtractor, removeDuplicates} from 'angular2/src/i18n/message_extractor'; +import {Message} from 'angular2/src/i18n/message'; import {Parser} from 'angular2/src/core/change_detection/parser/parser'; import {Lexer} from 'angular2/src/core/change_detection/parser/lexer'; diff --git a/modules/angular2/test/i18n/message_spec.ts b/modules/angular2/test/i18n/message_spec.ts new file mode 100644 index 000000000000..50d9e56cc1c2 --- /dev/null +++ b/modules/angular2/test/i18n/message_spec.ts @@ -0,0 +1,27 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xdescribe, + xit +} from 'angular2/testing_internal'; + +import {Message, id} from 'angular2/src/i18n/message'; + +export function main() { + ddescribe('Message', () => { + describe("id", () => { + it("should return a different id for messages with and without the meaning", () => { + let m1 = new Message("content", "meaning", null); + let m2 = new Message("content", null, null); + expect(id(m1)).toEqual(id(m1)); + expect(id(m1)).not.toEqual(id(m2)); + }); + }); + }); +} diff --git a/modules/angular2/test/i18n/xmb_serializer_spec.ts b/modules/angular2/test/i18n/xmb_serializer_spec.ts new file mode 100644 index 000000000000..1ef4d071c3c1 --- /dev/null +++ b/modules/angular2/test/i18n/xmb_serializer_spec.ts @@ -0,0 +1,35 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xdescribe, + xit +} from 'angular2/testing_internal'; + +import {Message, id} from 'angular2/src/i18n/message'; +import {serialize} from 'angular2/src/i18n/xmb_serializer'; + +export function main() { + describe('Xmb Serialization', () => { + it("should return an empty message bundle for an empty list of messages", + () => { expect(serialize([])).toEqual(""); }); + + it("should serialize messages without desc", () => { + let m = new Message("content", "meaning", null); + let expected = `content`; + expect(serialize([m])).toEqual(expected); + }); + + it("should serialize messages with desc", () => { + let m = new Message("content", "meaning", "description"); + let expected = + `content`; + expect(serialize([m])).toEqual(expected); + }); + }); +} diff --git a/modules/benchmarks_external/pubspec.yaml b/modules/benchmarks_external/pubspec.yaml index e7cf10f4765c..a488a5d87b8d 100644 --- a/modules/benchmarks_external/pubspec.yaml +++ b/modules/benchmarks_external/pubspec.yaml @@ -2,7 +2,7 @@ name: benchmarks_external environment: sdk: '>=1.4.0' dependencies: - angular: '>=1.0.0 <2.0.0' + angular: '>=1.1.2+2 <2.0.0' browser: '>=0.10.0 <0.11.0' dev_dependencies: angular2: diff --git a/modules_dart/transform/lib/extract_messages.dart b/modules_dart/transform/lib/extract_messages.dart new file mode 100644 index 000000000000..e4cdbdc71c2b --- /dev/null +++ b/modules_dart/transform/lib/extract_messages.dart @@ -0,0 +1,119 @@ +import 'package:build/build.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'src/transform/common/url_resolver.dart'; +import 'dart:async'; +import 'package:angular2/i18n.dart'; +import 'package:angular2/src/core/change_detection/parser/parser.dart'; +import 'package:angular2/src/core/change_detection/parser/lexer.dart'; +import 'package:angular2/src/core/reflection/reflector.dart'; +import 'package:angular2/src/core/reflection/reflection_capabilities.dart'; +import 'package:angular2/src/compiler/html_parser.dart'; + +/** + * An command-line utility extracting i18n messages from an application. + * + * For instance, the following command will extract all the messages from the 'my-app-package' package, where + * index.dart is the entry point, and will serialize them into i18n-messages.xml. + * + * pub run packages/angular2/extract_messages.dart 'my-app-package' 'web/src/index.dart' 'i18n-messages.xml' + */ +main(List args) async { + final input = new InputSet(args[0], [args[1]]); + final output = new AssetId(args[0], args[2]); + + await build(new PhaseGroup.singleAction(new I18nMessageExtractorBuilder(output), input)); +} + +class I18nMessageExtractorBuilder implements Builder { + final AssetId outputAssetId; + + I18nMessageExtractorBuilder(this.outputAssetId); + + Future build(BuildStep buildStep) async { + final resolver = await buildStep.resolve(buildStep.input.id); + final entryLib = resolver.getLibrary(buildStep.input.id); + + final extractor = new I18nMessageExtractor((path) => buildStep.readAsString(path)); + await extractor.processLibrary(entryLib); + resolver.release(); + + if (extractor.errors.length > 0) { + print("Errors:"); + extractor.errors.forEach(print); + throw "Failed to extract messages"; + + } else { + await buildStep.writeAsString(new Asset(outputAssetId, extractor.output)); + } + } + + List declareOutputs(AssetId inputId) => [outputAssetId]; +} + +class I18nMessageExtractor { + final TransformerUrlResolver urlResovler = new TransformerUrlResolver(); + final List messages = []; + final List errors = []; + final HtmlParser htmlParser = new HtmlParser(); + final Parser parser = new Parser(new Lexer(), new Reflector(new ReflectionCapabilities())); + + final Function readInput; + + I18nMessageExtractor(this.readInput); + + String get output => serialize(removeDuplicates(messages)); + + Future processLibrary(LibraryElement el) async { + return Future.wait(el.units.map(processCompilationUnit)); + } + + Future processCompilationUnit(CompilationUnitElement el) async { + return Future.wait(el.types.map(processClass)); + } + + Future processClass(ClassElement el) async { + final baseUrl = (el.source as dynamic).assetId; + final filtered = el.metadata.where((m) { + if (m.element is ConstructorElement) { + final isComponent = m.element.enclosingElement.name == "Component" && + m.element.library.displayName == "angular2.src.core.metadata"; + + final isView = m.element.enclosingElement.name == "View" && + m.element.library.displayName == "angular2.src.core.metadata"; + + return isComponent || isView; + } else { + return false; + } + }); + + return Future.wait(filtered.map((m) => processAnnotation(el, m, baseUrl))); + } + + Future processAnnotation(ClassElement el, ElementAnnotation m, baseUrl) async { + final fields = (m.constantValue as dynamic).fields["(super)"].fields; + final template = fields["template"]; + final templateUrl = fields["templateUrl"]; + + if (template != null && !template.isNull) { + processTemplate(template.toStringValue(), baseUrl.toString()); + } + + if (templateUrl != null && !templateUrl.isNull) { + final value = templateUrl.toStringValue(); + final resolvedPath = urlResovler.resolve(toAssetUri(baseUrl), value); + final template = await readInput(fromUri(resolvedPath)); + processTemplate(template.toStringValue(), baseUrl.toString()); + } + } + + void processTemplate(String template, String sourceUrl) { + final m = new MessageExtractor(htmlParser, parser); + final res = m.extract(template, sourceUrl); + if (res.errors.isNotEmpty) { + errors.addAll(res.errors); + } else { + messages.addAll(res.messages); + } + } +} \ No newline at end of file