i18nliner-js (part II)
this swaps out (most of) our runtime i18n.js hacks with i18nliner-js slightly tweak our js htmlEscape implementation (but not behavior) to play nicely with i18nliner-js remove auto-scoping abilities of I18n.lookup, but that's not a huge deal as we didn't really need it test plan: 1. verify english defaults: 1. use canvas in english 2. confirm everything looks correct 2. verify translation keys/scopes: 1. run canvas w/ RAILS_LOAD_ALL_LOCALES=true and optimized js 2. use canvas in spanish 3. confirm that todo está bien 3. confirm you can now use i18nliner-y features: 1. call `I18n.t` without a key Change-Id: I93a2763f638f2807a7f804d320409fbdc80f0454 Reviewed-on: https://gerrit.instructure.com/42895 Reviewed-by: Michael Ziwisky <mziwisky@instructure.com> Product-Review: Jennifer Stern <jstern@instructure.com> QA-Review: Matt Fairbourn <mfairbourn@instructure.com> Tested-by: Jenkins <jenkins@instructure.com>
This commit is contained in:
parent
9fbea5bd09
commit
2d37c16193
|
@ -27,9 +27,8 @@ define [
|
|||
wrappers[new Array(parseInt(key.replace('w', '')) + 2).join('*')] = value
|
||||
delete options[key]
|
||||
options.wrapper = wrappers if wrappers['*']
|
||||
options.needsEscaping = true
|
||||
options = $.extend(options, this) unless this instanceof String or typeof this is 'string'
|
||||
I18n.scoped(scope).t(translationKey, defaultValue, options)
|
||||
htmlEscape I18n.scoped(scope).t(translationKey, defaultValue, options)
|
||||
|
||||
hiddenIf : (condition) -> " display:none; " if condition
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ define [
|
|||
|
||||
year = (new Date()).getFullYear()
|
||||
position = {year: 1, month: 2, day: 3}
|
||||
dateSettings = I18n.lookup('#date')
|
||||
dateSettings = I18n.lookup('date')
|
||||
|
||||
if options.type is 'birthdate'
|
||||
_.defaults options,
|
||||
|
|
|
@ -9,7 +9,7 @@ define [
|
|||
if strings.length > cutoff
|
||||
strings = strings[0...cutoff].concat([strings[cutoff...strings.length]])
|
||||
$.toSentence(for strOrArray in strings
|
||||
if typeof strOrArray is 'string' or strOrArray._icHTMLSafe
|
||||
if typeof strOrArray is 'string' or strOrArray instanceof h.SafeString
|
||||
"<span>#{h(strOrArray)}</span>"
|
||||
else
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
define(function(require) {
|
||||
var I18n = require('i18n!something');
|
||||
describe('I18n.t', function() {
|
||||
it('should work with just a string default', function() {
|
||||
expect(I18n.t('Foo')).toBe('Foo');
|
||||
});
|
||||
|
||||
it('should work with a default object and an options object', function() {
|
||||
expect(I18n.t({one: "1 person", other: "%{count} people"}, {count: 2})).toBe('2 people');
|
||||
});
|
||||
|
||||
it('should work with two params', function() {
|
||||
expect(I18n.t('foo', 'Foo')).toBe('Foo');
|
||||
});
|
||||
|
@ -38,4 +46,4 @@ define(function(require) {
|
|||
})).toBe('3 students');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
define([], function() {
|
||||
var INTERPOLATER = /\%\{([^\}]+)\}/g;
|
||||
var KEY_PATTERN = /^\#?\w+(\.\w+)+$/; // handle our absolute keys
|
||||
var COUNT_KEY_MAP = ["zero", "one"];
|
||||
|
||||
var i18n = {
|
||||
interpolate: function(contents, options) {
|
||||
|
@ -15,56 +17,51 @@ define([], function() {
|
|||
return contents;
|
||||
},
|
||||
|
||||
load : function(name, req, onLoad) {
|
||||
isKeyProvided: function(keyOrDefault, defaultOrOptions, maybeOptions) {
|
||||
if (typeof keyOrDefault === 'object')
|
||||
return false;
|
||||
if (typeof defaultOrOptions === 'string')
|
||||
return true;
|
||||
if (maybeOptions)
|
||||
return true;
|
||||
if (typeof keyOrDefault === 'string' && keyOrDefault.match(this.keyPattern))
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
inferArguments: function(args) {
|
||||
var hasKey = this.isKeyProvided.apply(this, args);
|
||||
if (hasKey) args = args.slice(1);
|
||||
return args;
|
||||
},
|
||||
|
||||
load: function(name, req, onLoad) {
|
||||
// Development only.
|
||||
// This gets replaced by Canvas I18n when embedded.
|
||||
//
|
||||
// Adapted/simplified from i18nliner-js and canvas' i18nObj
|
||||
//
|
||||
// Returns the defaultValue you provide with variables interpolated,
|
||||
// if specified.
|
||||
//
|
||||
// See the project README for i18n work.
|
||||
var t = function(__key__, defaultValue, options) {
|
||||
var value;
|
||||
|
||||
if (arguments.length === 2) {
|
||||
if (typeof defaultValue === 'string') {
|
||||
options = { defaultValue: defaultValue };
|
||||
}
|
||||
else if (typeof defaultValue === 'object') {
|
||||
options = defaultValue;
|
||||
}
|
||||
else {
|
||||
throw new Error("Bad I18n.t() call, expected an options object or a defaultValue string.");
|
||||
}
|
||||
}
|
||||
else if (arguments.length === 3 && !options.defaultValue) {
|
||||
options.defaultValue = defaultValue;
|
||||
var t = function() {
|
||||
var args = i18n.inferArguments([].slice.call(arguments));
|
||||
var defaultValue = args[0];
|
||||
var options = args[1] || {};
|
||||
var countKey;
|
||||
|
||||
if (typeof defaultValue !== 'string' && typeof defaultValue !== 'object') {
|
||||
throw new Error("Bad I18n.t() call, expected a default string or object.");
|
||||
}
|
||||
|
||||
if (options.hasOwnProperty('count') && typeof defaultValue === 'object') {
|
||||
switch(options.count) {
|
||||
case 0:
|
||||
if (defaultValue.zero) {
|
||||
options.defaultValue = defaultValue.zero;
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (defaultValue.one) {
|
||||
options.defaultValue = defaultValue.one;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (defaultValue.other) {
|
||||
options.defaultValue = defaultValue.other;
|
||||
}
|
||||
}
|
||||
countKey = COUNT_KEY_MAP[options.count];
|
||||
defaultValue = defaultValue[countKey] || defaultValue.other;
|
||||
}
|
||||
|
||||
value = i18n.interpolate(''+options.defaultValue, options);
|
||||
|
||||
return value;
|
||||
return i18n.interpolate(''+defaultValue, options);
|
||||
};
|
||||
|
||||
var l = function(scope, value) {
|
||||
|
@ -79,4 +76,4 @@ define([], function() {
|
|||
};
|
||||
|
||||
return i18n;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,23 +1,13 @@
|
|||
define([
|
||||
'vendor/i18n',
|
||||
'vendor/i18n_js_extension',
|
||||
'jquery',
|
||||
'str/htmlEscape',
|
||||
'str/pluralize',
|
||||
'str/escapeRegex',
|
||||
'compiled/str/i18nLolcalize',
|
||||
'vendor/date' /* Date.parse, Date.UTC */
|
||||
], function(I18n, $, htmlEscape, pluralize, escapeRegex, i18nLolcalize) {
|
||||
|
||||
// Export globally for tinymce/specs
|
||||
window.I18n = I18n;
|
||||
], function(I18n, $, htmlEscape, i18nLolcalize) {
|
||||
|
||||
I18n.locale = document.documentElement.getAttribute('lang');
|
||||
|
||||
// Set the placeholder format. Accepts `%{placeholder}` and %h{placeholder}.
|
||||
// %h{placeholder} indicate it is an htmlSafe value, (e.g. an input) and
|
||||
// anything not already safe should be html-escaped
|
||||
I18n.PLACEHOLDER = /%h?\{(.*?)\}/gm;
|
||||
|
||||
I18n.isValidNode = function(obj, node) {
|
||||
// handle names like "foo.bar.baz"
|
||||
var nameParts = node.split('.');
|
||||
|
@ -61,23 +51,20 @@ I18n.lookup = function(scope, options) {
|
|||
return messages;
|
||||
};
|
||||
|
||||
I18n.interpolate = function(message, options) {
|
||||
var placeholder, value, name, matches, needsEscaping = false, htmlSafe;
|
||||
|
||||
// i18nliner-js overrides interpolate with a wrapper-and-html-safety-aware
|
||||
// version, so we need to override the now-renamed original
|
||||
I18n.interpolateWithoutHtmlSafety = function(message, options) {
|
||||
options = this.prepareOptions(options);
|
||||
if (options.wrapper) {
|
||||
needsEscaping = true;
|
||||
message = this.applyWrappers(message, options.wrapper);
|
||||
}
|
||||
if (options.needsEscaping) {
|
||||
needsEscaping = true;
|
||||
var matches = message.match(this.PLACEHOLDER);
|
||||
|
||||
if (!matches) {
|
||||
return message;
|
||||
}
|
||||
|
||||
matches = message.match(this.PLACEHOLDER) || [];
|
||||
var placeholder, value, name;
|
||||
|
||||
for (var i = 0; placeholder = matches[i]; i++) {
|
||||
name = placeholder.replace(this.PLACEHOLDER, "$1");
|
||||
htmlSafe = (placeholder[1] === 'h'); // e.g. %h{input}
|
||||
|
||||
// handle names like "foo.bar.baz"
|
||||
var nameParts = name.split('.');
|
||||
|
@ -89,14 +76,6 @@ I18n.interpolate = function(message, options) {
|
|||
if (!this.isValidNode(options, name)) {
|
||||
value = "[missing " + placeholder + " value]";
|
||||
}
|
||||
if (needsEscaping) {
|
||||
if (!value._icHTMLSafe && !htmlSafe) {
|
||||
value = htmlEscape(value);
|
||||
}
|
||||
} else if (value._icHTMLSafe || htmlSafe) {
|
||||
needsEscaping = true;
|
||||
message = htmlEscape(message);
|
||||
}
|
||||
|
||||
regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
|
||||
message = message.replace(regex, value);
|
||||
|
@ -105,31 +84,6 @@ I18n.interpolate = function(message, options) {
|
|||
return message;
|
||||
};
|
||||
|
||||
I18n.wrapperRegexes = {};
|
||||
|
||||
I18n.applyWrappers = function(string, wrappers) {
|
||||
var keys = [];
|
||||
var key;
|
||||
|
||||
string = htmlEscape(string);
|
||||
if (typeof(wrappers) == "string") {
|
||||
wrappers = {'*': wrappers};
|
||||
}
|
||||
for (key in wrappers) {
|
||||
keys.push(key);
|
||||
}
|
||||
keys.sort().reverse();
|
||||
for (var i=0, l=keys.length; i < l; i++) {
|
||||
key = keys[i];
|
||||
if (!this.wrapperRegexes[key]) {
|
||||
var escapedKey = escapeRegex(key);
|
||||
this.wrapperRegexes[key] = new RegExp(escapedKey + "([^" + escapedKey + "]*)" + escapedKey, "g");
|
||||
}
|
||||
string = string.replace(this.wrapperRegexes[key], wrappers[key]);
|
||||
}
|
||||
return string;
|
||||
};
|
||||
|
||||
var _localize = I18n.localize;
|
||||
I18n.localize = function(scope, value) {
|
||||
var result = _localize.call(this, scope, value);
|
||||
|
@ -261,11 +215,18 @@ I18n.strftime = function(date, format) {
|
|||
return f;
|
||||
};
|
||||
|
||||
I18n.Utils.HtmlSafeString = htmlEscape.SafeString; // this is what we use elsewhere in canvas, so make i18nliner use it too
|
||||
I18n.CallHelpers.keyPattern = /^\#?\w+(\.\w+)+$/ // handle our absolute keys
|
||||
I18n.CallHelpers.normalizeKey = function(key, options) {
|
||||
if (key[0] === '#') {
|
||||
key = key.slice(1);
|
||||
delete options.scope;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
var normalizeDefault = function(str) { return str };
|
||||
if (window.ENV && window.ENV.lolcalize) {
|
||||
normalizeDefault = i18nLolcalize;
|
||||
I18n.CallHelpers.normalizeDefault = i18nLolcalize;
|
||||
}
|
||||
|
||||
I18n.scoped = function(scope, callback) {
|
||||
|
@ -279,36 +240,26 @@ I18n.scope = function(scope) {
|
|||
this.scope = scope;
|
||||
};
|
||||
I18n.scope.prototype = {
|
||||
resolveScope: function(key) {
|
||||
if (typeof(key) == "object") {
|
||||
key = key.join(I18n.defaultSeparator);
|
||||
}
|
||||
if (key[0] == '#') {
|
||||
return key.replace(/^#/, '');
|
||||
} else {
|
||||
return this.scope + I18n.defaultSeparator + key;
|
||||
HtmlSafeString: I18n.HtmlSafeString,
|
||||
|
||||
translate: function() {
|
||||
var args = [].slice.call(arguments);
|
||||
var options = args[args.length - 1];
|
||||
if (!(options instanceof Object)) {
|
||||
options = {}
|
||||
args.push(options);
|
||||
}
|
||||
options.scope = this.scope;
|
||||
return I18n.translate.apply(I18n, args);
|
||||
},
|
||||
translate: function(scope, defaultValue, options) {
|
||||
options = options || {};
|
||||
if (typeof(options.count) != 'undefined' && typeof(defaultValue) == "string" && defaultValue.match(/^[\w\-]+$/)) {
|
||||
defaultValue = pluralize.withCount(options.count, defaultValue);
|
||||
}
|
||||
options.defaultValue = normalizeDefault(defaultValue);
|
||||
return I18n.translate(this.resolveScope(scope), options);
|
||||
},
|
||||
localize: function(scope, value) {
|
||||
return I18n.localize(this.resolveScope(scope), value);
|
||||
},
|
||||
pluralize: function(count, scope, options) {
|
||||
return I18n.pluralize(count, this.resolveScope(scope), options);
|
||||
localize: function(key, date) {
|
||||
if (key[0] === '#') key = key.slice(1);
|
||||
return I18n.localize(key, date);
|
||||
},
|
||||
beforeLabel: function(text) {
|
||||
return this.t("#before_label_wrapper", "%{text}:", {'text': text});
|
||||
},
|
||||
lookup: function(scope, options) {
|
||||
return I18n.lookup(this.resolveScope(scope), options);
|
||||
},
|
||||
lookup: I18n.lookup.bind(I18n),
|
||||
toTime: I18n.toTime.bind(I18n),
|
||||
toNumber: I18n.toNumber.bind(I18n),
|
||||
toCurrency: I18n.toCurrency.bind(I18n),
|
||||
|
|
|
@ -66,12 +66,10 @@ define([
|
|||
};
|
||||
|
||||
// useful for i18n, e.g. t('key', 'pick one: %{select}', {select: $.raw('<select><option>...')})
|
||||
// note that raw returns a String object, so you may want to call toString
|
||||
// note that raw returns a SafeString object, so you may want to call toString
|
||||
// if you're using it elsewhere
|
||||
$.raw = function(str) {
|
||||
str = new String(str);
|
||||
str._icHTMLSafe = true;
|
||||
return str;
|
||||
return new htmlEscape.SafeString(str);
|
||||
}
|
||||
|
||||
$.replaceOneTag = function(text, name, value) {
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
define(['INST', 'jquery'], function(INST, $) {
|
||||
var dummy = $('<div/>');
|
||||
|
||||
function SafeString(string) {
|
||||
this.string = (typeof string === 'string' ? string : "" + string);
|
||||
}
|
||||
SafeString.prototype.toString = function() {
|
||||
return this.string;
|
||||
};
|
||||
|
||||
var htmlEscape = function(str) {
|
||||
return str && str._icHTMLSafe ?
|
||||
str.toString() :
|
||||
dummy.text(str).html();
|
||||
// ideally we should wrap this in a SafeString, but this is how it has
|
||||
// always worked :-/
|
||||
return dummy.text(str).html();
|
||||
}
|
||||
|
||||
// Escapes HTML tags from string, or object string props of `strOrObject`.
|
||||
|
@ -12,6 +19,8 @@ define(['INST', 'jquery'], function(INST, $) {
|
|||
var escape = function(strOrObject) {
|
||||
if (typeof strOrObject === 'string') {
|
||||
return htmlEscape(strOrObject);
|
||||
} else if (strOrObject instanceof SafeString) {
|
||||
return strOrObject;
|
||||
}
|
||||
|
||||
var k, v;
|
||||
|
@ -22,7 +31,8 @@ define(['INST', 'jquery'], function(INST, $) {
|
|||
}
|
||||
}
|
||||
return strOrObject;
|
||||
}
|
||||
};
|
||||
escape.SafeString = SafeString;
|
||||
|
||||
// tinymce plugins use this and they need it global :(
|
||||
INST.htmlEscape = escape;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,6 @@
|
|||
# note: most of these tests are now redundant w/ i18nliner-js, leaving them
|
||||
# for a little bit though
|
||||
|
||||
define [
|
||||
"jquery"
|
||||
"i18nObj"
|
||||
|
@ -21,7 +24,7 @@ define [
|
|||
originalLocale = I18n.locale
|
||||
try
|
||||
$.extend(true, I18n, {locale: 'bad-locale', translations: {en: {foo: {fallback_message: 'this is in the en locale'}}}})
|
||||
equal scope.lookup('fallback_message'),
|
||||
equal scope.lookup('foo.fallback_message'),
|
||||
'this is in the en locale'
|
||||
finally
|
||||
I18n.locale = originalLocale
|
||||
|
@ -32,11 +35,11 @@ define [
|
|||
|
||||
test "html safety: should html-escape translations and interpolations if any interpolated values are htmlSafe", ->
|
||||
equal t('bar', "only one of these won't get escaped: <input>, %{a}, %{b} & %{c}", {a: '<img>', b: $.raw('<br>'), c: '<hr>'}),
|
||||
'only one of these won\'t get escaped: <input>, <img>, <br> & <hr>'
|
||||
'only one of these won't get escaped: <input>, <img>, <br> & <hr>'
|
||||
|
||||
test "html safety: should html-escape translations and interpolations if any placeholders are flagged as safe", ->
|
||||
equal t('bar', "only one of these won't get escaped: <input>, %{a}, %h{b} & %{c}", {a: '<img>', b: '<br>', c: '<hr>'}),
|
||||
'only one of these won\'t get escaped: <input>, <img>, <br> & <hr>'
|
||||
'only one of these won't get escaped: <input>, <img>, <br> & <hr>'
|
||||
|
||||
test "wrappers: should auto-html-escape", ->
|
||||
equal t('bar', '*2* > 1', {wrapper: '<b>$1</b>'}),
|
||||
|
|
|
@ -38,8 +38,8 @@ describe "handlebars" do
|
|||
<li>{{#t "protip" type=../type}}Important {{type}} tip:{{/t}} {{this}}</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
<p>{{#t "html"}}lemme instructure you some html: if you type {{input}}, you get {{{input}}}{{/t}}</p>
|
||||
<p>{{#t "reversed"}}in other words you get {{{input}}} when you type {{input}}{{/t}}</p>
|
||||
<p>{{#t "html"}}lemme instructure you some html: if you type {{input}}, you get {{{raw_input}}}{{/t}}</p>
|
||||
<p>{{#t "reversed"}}in other words you get {{{raw_input}}} when you type {{input}}{{/t}}</p>
|
||||
<p>{{#t "escapage"}}this is {{escaped}}{{/t}}</p>
|
||||
<p>{{#t "unescapage"}}this is {{{unescaped}}}{{/t}}</p>
|
||||
{{#t "bye"}}welp, see you l8r! dont forget 2 <a href="{{url}}">like us</a> on facebook lol{{/t}}
|
||||
|
@ -51,6 +51,8 @@ describe "handlebars" do
|
|||
type: 'yoga',
|
||||
items: ['dont forget to stretch!!!'],
|
||||
input: '<input>',
|
||||
raw_input: '<input>', # note; this is temporary due to a change in the html-safety implementation.
|
||||
# once i18nliner-handlebars lands, the old spec will pass
|
||||
url: 'http://foo.bar',
|
||||
escaped: '<b>escaped</b>',
|
||||
unescaped: '<b>unescaped</b>'
|
||||
|
@ -129,7 +131,7 @@ describe "handlebars" do
|
|||
<b> Je voudrais un croissant</b>
|
||||
</p>
|
||||
<p>
|
||||
<i> Yes, that's true, he would </i>
|
||||
<i> Yes, that's true, he would </i>
|
||||
</p>
|
||||
HTML
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue