diff --git a/app/coffeescripts/handlebars_helpers.coffee b/app/coffeescripts/handlebars_helpers.coffee index 50fd3cc99d4..31886931d05 100644 --- a/app/coffeescripts/handlebars_helpers.coffee +++ b/app/coffeescripts/handlebars_helpers.coffee @@ -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 diff --git a/app/coffeescripts/util/dateSelect.coffee b/app/coffeescripts/util/dateSelect.coffee index 44e52cf7779..512adb6d20f 100644 --- a/app/coffeescripts/util/dateSelect.coffee +++ b/app/coffeescripts/util/dateSelect.coffee @@ -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, diff --git a/app/coffeescripts/util/listWithOthers.coffee b/app/coffeescripts/util/listWithOthers.coffee index 62ca6eb3afa..68acb5b024d 100644 --- a/app/coffeescripts/util/listWithOthers.coffee +++ b/app/coffeescripts/util/listWithOthers.coffee @@ -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 "#{h(strOrArray)}" else """ diff --git a/client_apps/canvas_quiz_statistics/test/unit/dev/i18n_test.js b/client_apps/canvas_quiz_statistics/test/unit/dev/i18n_test.js index ae8a5e16467..57a13018857 100644 --- a/client_apps/canvas_quiz_statistics/test/unit/dev/i18n_test.js +++ b/client_apps/canvas_quiz_statistics/test/unit/dev/i18n_test.js @@ -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'); }); }); -}); \ No newline at end of file +}); diff --git a/client_apps/canvas_quiz_statistics/vendor/js/require/i18n.js b/client_apps/canvas_quiz_statistics/vendor/js/require/i18n.js index af1f27580d3..cf8e94c5517 100644 --- a/client_apps/canvas_quiz_statistics/vendor/js/require/i18n.js +++ b/client_apps/canvas_quiz_statistics/vendor/js/require/i18n.js @@ -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; -}); \ No newline at end of file +}); diff --git a/public/javascripts/i18nObj.js b/public/javascripts/i18nObj.js index 2e4144b447e..a9f4fd6f2cd 100644 --- a/public/javascripts/i18nObj.js +++ b/public/javascripts/i18nObj.js @@ -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), diff --git a/public/javascripts/jquery.instructure_misc_helpers.js b/public/javascripts/jquery.instructure_misc_helpers.js index 3730783e935..3e484bddcd2 100644 --- a/public/javascripts/jquery.instructure_misc_helpers.js +++ b/public/javascripts/jquery.instructure_misc_helpers.js @@ -66,12 +66,10 @@ define([ }; // useful for i18n, e.g. t('key', 'pick one: %{select}', {select: $.raw(', %{a}, %{b} & %{c}", {a: '', b: $.raw('
'), c: '
'}), - 'only one of these won\'t get escaped: <input>, <img>,
& <hr>' + 'only one of these won't get escaped: <input>, <img>,
& <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: , %{a}, %h{b} & %{c}", {a: '', b: '
', c: '
'}), - 'only one of these won\'t get escaped: <input>, <img>,
& <hr>' + 'only one of these won't get escaped: <input>, <img>,
& <hr>' test "wrappers: should auto-html-escape", -> equal t('bar', '*2* > 1', {wrapper: '$1'}), diff --git a/spec/selenium/handlebars_spec.rb b/spec/selenium/handlebars_spec.rb index b612625b046..c8384829090 100644 --- a/spec/selenium/handlebars_spec.rb +++ b/spec/selenium/handlebars_spec.rb @@ -38,8 +38,8 @@ describe "handlebars" do
  • {{#t "protip" type=../type}}Important {{type}} tip:{{/t}} {{this}}
  • {{/each}} -

    {{#t "html"}}lemme instructure you some html: if you type {{input}}, you get {{{input}}}{{/t}}

    -

    {{#t "reversed"}}in other words you get {{{input}}} when you type {{input}}{{/t}}

    +

    {{#t "html"}}lemme instructure you some html: if you type {{input}}, you get {{{raw_input}}}{{/t}}

    +

    {{#t "reversed"}}in other words you get {{{raw_input}}} when you type {{input}}{{/t}}

    {{#t "escapage"}}this is {{escaped}}{{/t}}

    {{#t "unescapage"}}this is {{{unescaped}}}{{/t}}

    {{#t "bye"}}welp, see you l8r! dont forget 2 like us on facebook lol{{/t}} @@ -51,6 +51,8 @@ describe "handlebars" do type: 'yoga', items: ['dont forget to stretch!!!'], input: '', + raw_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: 'escaped', unescaped: 'unescaped' @@ -129,7 +131,7 @@ describe "handlebars" do Je voudrais un croissant

    - Yes, that's true, he would + Yes, that's true, he would

    HTML end