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