Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit d9c80a2

Browse files
committed
feat($interpolate): MessageFormat extensions
Extend interpolation with MessageFormat like syntax. Ref: <https://docs.google.com/a/google.com/document/d/1pbtW2yvtmFBikfRrJd8VAsabiFkKezmYZ_PbgdjQOVU/edit> Example: ```html {{recipients.length, plural, offset:1 =0 {You gave no gifts} =1 { {{ recipients[0].gender, select, male {You gave him a gift.} female {You gave her a gift.} other {You gave them a gift.} }} } one { {{ recipients[0].gender, select, male {You gave him and one other person a gift.} female {You gave her and one other person a gift.} other {You gave them and one other person a gift.} }} } other {You gave {{recipients.length}} people gifts. ({{name}}) } }} ``` Quick note on syntax differences from MessageFormat: - MessageFormat directives are always inside {{ }} instead of single { }. This ensures a consistent interpolation syntax (else you could interpolate in more than one way and have to pick one based on feature availability for that syntax.) - The first word inside such syntax can be an arbitrary Angular expression instead of a single identifier. - You can nest them as deep as you want. As mentioned earlier, you would use {{ }} to start the nested interpolation that may optionally include select/plural extensions. - Only "select" and "plural" keywords are currently recognized. - Quoting support is coming in a future commit. - Positional arguments/placeholders are not supported. They don't make sense in Angular templates anyway (they are only helpful when using API calls from a programming language.) - The "#" character is not special in plural messages. This will be supported in a future commit. However, since you can embed any Angular expression inside the message, you can still play around with it. (Also, the "#" symbol doesn't nest with plurals if you're wanting to write full messages. Alternative syntax would be nice.)
1 parent c9a4421 commit d9c80a2

File tree

4 files changed

+505
-48
lines changed

4 files changed

+505
-48
lines changed

angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var angularFiles = {
2828
'src/ng/locale.js',
2929
'src/ng/location.js',
3030
'src/ng/log.js',
31+
'src/ng/messageformat.js',
3132
'src/ng/parse.js',
3233
'src/ng/q.js',
3334
'src/ng/raf.js',

src/AngularPublic.js

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ function publishExternalAPI(angular) {
224224
$httpBackend: $HttpBackendProvider,
225225
$location: $LocationProvider,
226226
$log: $LogProvider,
227+
$$messageFormat: $$MessageFormatProvider,
227228
$parse: $ParseProvider,
228229
$rootScope: $RootScopeProvider,
229230
$q: $QProvider,

src/ng/interpolate.js

+95-48
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var $interpolateMinErr = minErr('$interpolate');
4040
function $InterpolateProvider() {
4141
var startSymbol = '{{';
4242
var endSymbol = '}}';
43+
var fastIsMsgFormatTestRe = /,\s*(?:select|plural)\s*,/;
4344

4445
/**
4546
* @ngdoc method
@@ -78,7 +79,7 @@ function $InterpolateProvider() {
7879
};
7980

8081

81-
this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) {
82+
this.$get = ['$parse', '$exceptionHandler', '$sce', '$$messageFormat', function($parse, $exceptionHandler, $sce, $$messageFormat) {
8283
var startSymbolLength = startSymbol.length,
8384
endSymbolLength = endSymbol.length,
8485
escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'),
@@ -110,6 +111,50 @@ function $InterpolateProvider() {
110111
return value;
111112
}
112113

114+
// Interpolation expressions support MessageFormat like extensions for plural and
115+
// select(gender). Delegate to $$messageFormat is such extensions are being used.
116+
function parseExtendedExpression(text, interceptorFn) {
117+
if (text.search(fastIsMsgFormatTestRe) == -1) {
118+
return $parse(text, interceptorFn);
119+
} else {
120+
return $$messageFormat.parse(text);
121+
}
122+
}
123+
124+
function findEndIndex(text, startIndex) {
125+
var endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength);
126+
if (endIndex == -1) {
127+
return endIndex;
128+
}
129+
var nestingLevel = 0;
130+
startIndex = text.indexOf(startSymbol, startIndex + startSymbolLength);
131+
for (;;) {
132+
if (startIndex == -1 || startIndex >= endIndex + endSymbolLength) {
133+
if (nestingLevel == 0) {
134+
return endIndex;
135+
}
136+
nestingLevel--;
137+
endIndex = text.indexOf(endSymbol, endIndex + endSymbolLength);
138+
if (endIndex == -1) {
139+
return -1;
140+
}
141+
} else {
142+
nestingLevel++;
143+
startIndex = text.indexOf(startSymbol, startIndex + startSymbolLength);
144+
}
145+
}
146+
}
147+
148+
// No messageformat support when we
149+
// 1. Can't nest expressions.
150+
// 2. Have redefined the startSymbol/endSymbol.
151+
// (2) will go away with updates to the $messageFormat service, but (1) will always remain.
152+
if (startSymbol == endSymbol || (startSymbol != "{{" || endSymbol != "}}")) {
153+
findEndIndex = function findEndIndex(text, startIndex) {
154+
return text.indexOf(endSymbol, startIndex + startSymbolLength);
155+
}
156+
}
157+
113158
/**
114159
* @ngdoc service
115160
* @name $interpolate
@@ -218,13 +263,13 @@ function $InterpolateProvider() {
218263

219264
while (index < textLength) {
220265
if (((startIndex = text.indexOf(startSymbol, index)) != -1) &&
221-
((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) {
266+
((endIndex = findEndIndex(text, startIndex)) != -1)) {
222267
if (index !== startIndex) {
223268
concat.push(unescapeText(text.substring(index, startIndex)));
224269
}
225270
exp = text.substring(startIndex + startSymbolLength, endIndex);
226271
expressions.push(exp);
227-
parseFns.push($parse(exp, parseStringifyInterceptor));
272+
parseFns.push(parseExtendedExpression(exp, parseStringifyInterceptor));
228273
index = endIndex + endSymbolLength;
229274
expressionPositions.push(concat.length);
230275
concat.push('');
@@ -250,54 +295,56 @@ function $InterpolateProvider() {
250295
"required. See http://docs.angularjs.org/api/ng.$sce", text);
251296
}
252297

253-
if (!mustHaveExpression || expressions.length) {
254-
var compute = function(values) {
255-
for (var i = 0, ii = expressions.length; i < ii; i++) {
256-
if (allOrNothing && isUndefined(values[i])) return;
257-
concat[expressionPositions[i]] = values[i];
258-
}
259-
return concat.join('');
260-
};
261-
262-
var getValue = function(value) {
263-
return trustedContext ?
264-
$sce.getTrusted(trustedContext, value) :
265-
$sce.valueOf(value);
266-
};
267-
268-
return extend(function interpolationFn(context) {
269-
var i = 0;
270-
var ii = expressions.length;
271-
var values = new Array(ii);
272-
273-
try {
274-
for (; i < ii; i++) {
275-
values[i] = parseFns[i](context);
276-
}
277-
278-
return compute(values);
279-
} catch (err) {
280-
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
281-
err.toString());
282-
$exceptionHandler(newErr);
298+
if (mustHaveExpression && expressions.length == 0) {
299+
return null;
300+
}
301+
302+
var compute = function(values) {
303+
for (var i = 0, ii = expressions.length; i < ii; i++) {
304+
if (allOrNothing && isUndefined(values[i])) return;
305+
concat[expressionPositions[i]] = values[i];
306+
}
307+
return concat.join('');
308+
};
309+
310+
var getValue = function(value) {
311+
return trustedContext ?
312+
$sce.getTrusted(trustedContext, value) :
313+
$sce.valueOf(value);
314+
};
315+
316+
return extend(function interpolationFn(context) {
317+
var i = 0;
318+
var ii = expressions.length;
319+
var values = new Array(ii);
320+
321+
try {
322+
for (; i < ii; i++) {
323+
values[i] = parseFns[i](context);
283324
}
284325

285-
}, {
286-
// all of these properties are undocumented for now
287-
exp: text, //just for compatibility with regular watchers created via $watch
288-
expressions: expressions,
289-
$$watchDelegate: function(scope, listener, objectEquality) {
290-
var lastValue;
291-
return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
292-
var currValue = compute(values);
293-
if (isFunction(listener)) {
294-
listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
295-
}
296-
lastValue = currValue;
297-
}, objectEquality);
326+
return compute(values);
327+
} catch (err) {
328+
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
329+
err.toString());
330+
$exceptionHandler(newErr);
298331
}
299-
});
300-
}
332+
333+
}, {
334+
// all of these properties are undocumented for now
335+
exp: text, //just for compatibility with regular watchers created via $watch
336+
expressions: expressions,
337+
$$watchDelegate: function(scope, listener, objectEquality) {
338+
var lastValue;
339+
return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
340+
var currValue = compute(values);
341+
if (isFunction(listener)) {
342+
listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
343+
}
344+
lastValue = currValue;
345+
}, objectEquality);
346+
}
347+
});
301348

302349
function parseStringifyInterceptor(value) {
303350
try {

0 commit comments

Comments
 (0)