From 6d8bac05b4a730a6cc85d73370704c934819abf7 Mon Sep 17 00:00:00 2001 From: Jon Jensen Date: Fri, 17 Oct 2014 16:41:07 -0600 Subject: [PATCH] i18nliner-handlebars (part I) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extraction and runtime for vanilla handlebars. ember will be part II because its strings currently don't even get extracted on master ¯\_(ツ)_/¯ differences in generated yml: 1. `%h{...}` placeholders from hbs are just `%{...}`, since html-safety is inferred at runtime without needing a placeholder hint (6 occcurrences) 2. inline `{{t ...}}` calls are now extracted (6 occurrences) 3. some trivial whitespace difference around some wrappers (2 occurrences) 4. html-entities are correctly converted into unicode equivalents, e.g. `Move To…` -> `Move To…` (1 occurrence) test plan: 1. verify string extraction: 1. `rake js:generate i18n:generate` before and after this commit 2. confirm `config/locales/generated/en.yml` is identical, except the differences listed above 2. verify js translation file generation: 1. `rake i18n:generate_js` before and after this commit 2. confirm the files in public/javascripts/translations are identical 3. verify hbs translation keys/scope behavior at runtime: 1. run canvas w/ RAILS_LOAD_ALL_LOCALES=true and optimized js 2. use canvas in spanish 3. confirm that todo está bien 4. confirm you can now use i18nliner-y features: 1. block helper with no key `{{#t}}hello world{{/t}}` 2. inline helper with no key `{{t "hello world"}}` Change-Id: Ic2a2c5cf102ca482919cbb91ac1c154467029685 Reviewed-on: https://gerrit.instructure.com/42942 Reviewed-by: Jennifer Stern Product-Review: Jennifer Stern QA-Review: Matt Fairbourn Tested-by: Jenkins --- app/coffeescripts/handlebars_helpers.coffee | 21 ++- .../registration/signupDialog.coffee | 16 ++- .../jst/registration/parentDialog.handlebars | 2 +- .../jst/registration/studentDialog.handlebars | 2 +- .../jst/registration/teacherDialog.handlebars | 2 +- gems/canvas_i18nliner/bin/prepare_hbs | 40 ++++++ gems/canvas_i18nliner/js/main.js | 24 +++- .../js/scoped_hbs_extractor.js | 35 +++++ .../js/scoped_hbs_pre_processor.js | 37 +++++ .../js/scoped_i18n_js_extractor.js | 4 +- .../js/scoped_translate_call.js | 39 ++--- gems/canvas_i18nliner/package.json | 4 +- .../lib/handlebars_tasks/handlebars.rb | 51 +++---- .../spec/handlebars_tasks/handlebars_spec.rb | 70 --------- .../lib/i18n_extraction/abstract_extractor.rb | 2 +- .../i18n_extraction/i18nliner_extensions.rb | 2 + gems/i18n_tasks/lib/tasks/i18n.rake | 135 ++---------------- public/javascripts/i18nObj.js | 43 ------ spec/selenium/handlebars_spec.rb | 8 +- 19 files changed, 220 insertions(+), 317 deletions(-) create mode 100755 gems/canvas_i18nliner/bin/prepare_hbs create mode 100644 gems/canvas_i18nliner/js/scoped_hbs_extractor.js create mode 100644 gems/canvas_i18nliner/js/scoped_hbs_pre_processor.js delete mode 100644 gems/handlebars_tasks/spec/handlebars_tasks/handlebars_spec.rb mode change 100755 => 100644 gems/i18n_extraction/lib/i18n_extraction/abstract_extractor.rb diff --git a/app/coffeescripts/handlebars_helpers.coffee b/app/coffeescripts/handlebars_helpers.coffee index 31886931d05..bd80acd2252 100644 --- a/app/coffeescripts/handlebars_helpers.coffee +++ b/app/coffeescripts/handlebars_helpers.coffee @@ -18,17 +18,24 @@ define [ ], (tz, enrollmentName, Handlebars, I18n, $, _, htmlEscape, semanticDateRange, dateSelect, mimeClass, convertApiUserContent, textHelper) -> Handlebars.registerHelper name, fn for name, fn of { - t : (translationKey, defaultValue, options) -> + t : (args..., options) -> wrappers = {} options = options?.hash ? {} - scope = options.scope - delete options.scope for key, value of options when key.match(/^w\d+$/) wrappers[new Array(parseInt(key.replace('w', '')) + 2).join('*')] = value delete options[key] options.wrapper = wrappers if wrappers['*'] - options = $.extend(options, this) unless this instanceof String or typeof this is 'string' - htmlEscape I18n.scoped(scope).t(translationKey, defaultValue, options) + options[key] = this[key] for key in this + new Handlebars.SafeString htmlEscape(I18n.t(args..., options)) + + __i18nliner_escape: (val) -> + htmlEscape val + + __i18nliner_safe: (val) -> + new htmlEscape.SafeString(val) + + __i18nliner_concat: (args..., options) -> + args.join("") hiddenIf : (condition) -> " display:none; " if condition @@ -42,8 +49,8 @@ define [ localDatetime = $.datetimeString(datetime) titleText = localDatetime if ENV and ENV.CONTEXT_TIMEZONE and (ENV.TIMEZONE != ENV.CONTEXT_TIMEZONE) - localText = Handlebars.helpers.t('#helpers.local','Local') - courseText = Handlebars.helpers.t('#helpers.course', 'Course') + localText = I18n.t('#helpers.local','Local') + courseText = I18n.t('#helpers.course', 'Course') courseDatetime = $.datetimeString(datetime, timezone: ENV.CONTEXT_TIMEZONE) if localDatetime != courseDatetime titleText = "#{localText}: #{localDatetime}
#{courseText}: #{courseDatetime}" diff --git a/app/coffeescripts/registration/signupDialog.coffee b/app/coffeescripts/registration/signupDialog.coffee index 3fd02e6165b..0570277a4bd 100644 --- a/app/coffeescripts/registration/signupDialog.coffee +++ b/app/coffeescripts/registration/signupDialog.coffee @@ -16,14 +16,26 @@ define [ $nodes = {} templates = {teacherDialog, studentDialog, parentDialog} + # we do this in coffee because of this hbs 1.3 bug: + # https://github.com/wycats/handlebars.js/issues/748 + # https://github.com/fivetanley/i18nliner-handlebars/commit/55be26ff + termsHtml = ({terms_of_use_url, privacy_policy_url}) -> + I18n.t( + "teacher_dialog.agree_to_terms_and_pp" + "You agree to the *terms of use* and acknowledge the **privacy policy**." + wrappers: [ + "$1" + "$1" + ] + ) + signupDialog = (id, title) -> return unless templates[id] $node = $nodes[id] ?= $('
') $node.html templates[id]( account: ENV.ACCOUNT.registration_settings terms_required: ENV.ACCOUNT.terms_required - terms_url: ENV.ACCOUNT.terms_of_use_url - privacy_url: ENV.ACCOUNT.privacy_policy_url + terms_html: termsHtml(ENV.ACCOUNT) ) $node.find('.date-field').datetime_field() diff --git a/app/views/jst/registration/parentDialog.handlebars b/app/views/jst/registration/parentDialog.handlebars index 4cc9ea8d153..d7adec74b7c 100644 --- a/app/views/jst/registration/parentDialog.handlebars +++ b/app/views/jst/registration/parentDialog.handlebars @@ -29,7 +29,7 @@
diff --git a/app/views/jst/registration/studentDialog.handlebars b/app/views/jst/registration/studentDialog.handlebars index fd82e44e6f7..3c9b3c5ea86 100644 --- a/app/views/jst/registration/studentDialog.handlebars +++ b/app/views/jst/registration/studentDialog.handlebars @@ -35,7 +35,7 @@
diff --git a/app/views/jst/registration/teacherDialog.handlebars b/app/views/jst/registration/teacherDialog.handlebars index 0b36fd3efac..8367b670f85 100644 --- a/app/views/jst/registration/teacherDialog.handlebars +++ b/app/views/jst/registration/teacherDialog.handlebars @@ -17,7 +17,7 @@
diff --git a/gems/canvas_i18nliner/bin/prepare_hbs b/gems/canvas_i18nliner/bin/prepare_hbs new file mode 100755 index 00000000000..1a077adab5f --- /dev/null +++ b/gems/canvas_i18nliner/bin/prepare_hbs @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +var readline = require('readline'); +var Handlebars = require('handlebars'); +var ScopedHbsExtractor = require('../js/scoped_hbs_extractor'); +var PreProcessor = require('i18nliner-handlebars/dist/lib/pre_processor')['default']; + +// make sure necessary overrides are set up (e.g. HbsPreProcessor.normalizeInterpolationKey) +require("../js/main"); + +var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false +}); + +rl.on('line', function(line) { + var data = JSON.parse(line); + var path = data.path; + var source = data.source; + + try { + var translationCount = 0; + var ast = Handlebars.parse(source); + var extractor = new ScopedHbsExtractor(ast, {path: path}); + var scope = extractor.scope; + PreProcessor.scope = scope; + PreProcessor.process(ast); + extractor.forEach(function() { translationCount++; }); + + var result = Handlebars.precompile(ast); + var payload = {template: result, scope: scope, translationCount: translationCount}; + process.stdout.write(JSON.stringify(payload) + "\n"); + } + catch (e) { + e = e.message || e; + process.stdout.write(JSON.stringify({error: e}) + "\n"); + } +}); + diff --git a/gems/canvas_i18nliner/js/main.js b/gems/canvas_i18nliner/js/main.js index f44deb5c246..8c583b0c29b 100755 --- a/gems/canvas_i18nliner/js/main.js +++ b/gems/canvas_i18nliner/js/main.js @@ -1,26 +1,42 @@ var I18nliner = require("i18nliner")["default"]; var Commands = I18nliner.Commands; var Check = Commands.Check; + +// it auto-registers its processor +var I18nlinerHbs = require("i18nliner-handlebars"); + var JsProcessor = require("i18nliner/dist/lib/processors/js_processor")["default"]; +var HbsProcessor = require("i18nliner-handlebars/dist/lib/hbs_processor")["default"]; var CallHelpers = require("i18nliner/dist/lib/call_helpers")["default"]; var glob = require("glob"); - -// explict subdirs, to work around perf issues and symlinks: +// explict subdirs, to work around perf issues // https://github.com/jenseng/i18nliner-js/issues/7 -// https://github.com/jenseng/globby-js/issues/2 -JsProcessor.prototype.directories = ["public/javascripts"].concat(glob.sync("public/javascripts/plugins/*")); +JsProcessor.prototype.directories = ["public/javascripts"]; +HbsProcessor.prototype.directories = ["app/views/jst"]; +HbsProcessor.prototype.defaultPattern = "**/*.handlebars"; +require("./scoped_hbs_pre_processor"); var ScopedI18nJsExtractor = require("./scoped_i18n_js_extractor"); +var ScopedHbsExtractor = require("./scoped_hbs_extractor"); var ScopedTranslationHash = require("./scoped_translation_hash"); +// remove path stuff we don't want in the scope +var pathRegex = new RegExp( + "^" + HbsProcessor.prototype.directories[0] + "(/plugins/[^/]+)?/" +); +ScopedHbsExtractor.prototype.normalizePath = function(path) { + return path.replace(pathRegex, ""); +}; + var GenerateJs = require("./generate_js"); Commands.Generate_js = GenerateJs; // swap out the defaults for our scope-aware varieties Check.prototype.TranslationHash = ScopedTranslationHash; JsProcessor.prototype.I18nJsExtractor = ScopedI18nJsExtractor; +HbsProcessor.prototype.Extractor = ScopedHbsExtractor; CallHelpers.keyPattern = /^\#?\w+(\.\w+)+$/ // handle our absolute keys module.exports = { diff --git a/gems/canvas_i18nliner/js/scoped_hbs_extractor.js b/gems/canvas_i18nliner/js/scoped_hbs_extractor.js new file mode 100644 index 00000000000..cdf5f809861 --- /dev/null +++ b/gems/canvas_i18nliner/js/scoped_hbs_extractor.js @@ -0,0 +1,35 @@ +var HbsExtractor = require("i18nliner-handlebars/dist/lib/extractor")["default"]; + +var HbsTranslateCall = require("i18nliner-handlebars/dist/lib/t_call")["default"]; +var ScopedHbsTranslateCall = require("./scoped_translate_call")(HbsTranslateCall); + +function ScopedHbsExtractor(ast, options) { + this.inferI18nScope(options.path); + HbsExtractor.apply(this, arguments); +}; + +ScopedHbsExtractor.prototype = Object.create(HbsExtractor.prototype); +ScopedHbsExtractor.prototype.constructor = ScopedHbsExtractor; + +ScopedHbsExtractor.prototype.normalizePath = function(path) { + return path; +}; + +ScopedHbsExtractor.prototype.inferI18nScope = function(path) { + if (this.normalizePath) + path = this.normalizePath(path); + var scope = path.replace(/\.[^\.]+/, '') // remove extension + .replace(/^_/, '') // some hbs files have a leading _ + .replace(/([A-Z]+)([A-Z][a-z])/g,'$1_$2') // camel -> underscore + .replace(/([a-z\d])([A-Z])/g, '$1_$2') // ditto + .replace("-", "_") + .replace(/\/_?/g, '.') + .toLowerCase(); + this.scope = scope; +}; + +ScopedHbsExtractor.prototype.buildTranslateCall = function(sexpr) { + return new ScopedHbsTranslateCall(sexpr, this.scope); +}; + +module.exports = ScopedHbsExtractor; diff --git a/gems/canvas_i18nliner/js/scoped_hbs_pre_processor.js b/gems/canvas_i18nliner/js/scoped_hbs_pre_processor.js new file mode 100644 index 00000000000..5d2124b1d19 --- /dev/null +++ b/gems/canvas_i18nliner/js/scoped_hbs_pre_processor.js @@ -0,0 +1,37 @@ +var I18nlinerHbs = require("i18nliner-handlebars")["default"]; +var PreProcessor = require("i18nliner-handlebars/dist/lib/pre_processor")["default"]; +var Handlebars = require("handlebars"); +var AST = Handlebars.AST; +var StringNode = AST.StringNode; +var HashNode = AST.HashNode; + +// slightly more lax interpolation key format for hbs to support any +// existing translations (camel case and dot syntax, e.g. "foo.bar.baz") +PreProcessor.normalizeInterpolationKey = function(key) { + key = key.replace(/[^a-z0-9.]/gi, ' '); + key = key.trim(); + key = key.replace(/ +/g, '_'); + return key.substring(0, 32); +}; + +// add explicit scope to all t calls (post block -> inline transformation) +var _processStatement = PreProcessor.processStatement; +PreProcessor.processStatement = function(statement) { + statement = _processStatement.call(this, statement) || statement; + if (statement.type === 'mustache' && statement.id.string === 't') + return this.injectScope(statement); +} + +PreProcessor.injectScope = function(node) { + if (!node.hash) + node.hash = node.sexpr.hash = new HashNode([]); + pairs = node.hash.pairs; + // to match our .rb scoping behavior, don't scope inferred keys + if (pairs.length && pairs[pairs.length - 1][0] === "i18n_inferred_key") { + node.hash.pairs = pairs.slice(0, pairs.length - 1); + } + else { + node.hash.pairs = pairs.concat([["scope", new StringNode(this.scope)]]); + } + return node; +} diff --git a/gems/canvas_i18nliner/js/scoped_i18n_js_extractor.js b/gems/canvas_i18nliner/js/scoped_i18n_js_extractor.js index 6f43a7ee712..095aef6ff26 100644 --- a/gems/canvas_i18nliner/js/scoped_i18n_js_extractor.js +++ b/gems/canvas_i18nliner/js/scoped_i18n_js_extractor.js @@ -1,8 +1,8 @@ var Errors = require("i18nliner/dist/lib/errors")["default"]; Errors.register("UnscopedTranslateCall"); -var ScopedTranslateCall = require("./scoped_translate_call"); -var ScopedTranslationHash = require("./scoped_translation_hash"); +var TranslateCall = require("i18nliner/dist/lib/extractors/translate_call")["default"]; +var ScopedTranslateCall = require("./scoped_translate_call")(TranslateCall); var I18nJsExtractor = require("i18nliner/dist/lib/extractors/i18n_js_extractor")["default"]; diff --git a/gems/canvas_i18nliner/js/scoped_translate_call.js b/gems/canvas_i18nliner/js/scoped_translate_call.js index f9121f254d5..6bd616a429a 100644 --- a/gems/canvas_i18nliner/js/scoped_translate_call.js +++ b/gems/canvas_i18nliner/js/scoped_translate_call.js @@ -1,24 +1,25 @@ -var TranslateCall = require("i18nliner/dist/lib/extractors/translate_call")["default"]; +module.exports = function(TranslateCall) { + var ScopedTranslateCall = function() { + var args = [].slice.call(arguments); + this.scope = args.pop(); -function ScopedTranslateCall(line, method, args, scope) { - this.scope = scope; + TranslateCall.apply(this, arguments); + } - TranslateCall.call(this, line, method, args); -}; + ScopedTranslateCall.prototype = Object.create(TranslateCall.prototype); + ScopedTranslateCall.prototype.constructor = ScopedTranslateCall; -ScopedTranslateCall.prototype = Object.create(TranslateCall.prototype); -ScopedTranslateCall.prototype.constructor = ScopedTranslateCall; + ScopedTranslateCall.prototype.normalizeKey = function(key) { + if (key[0] === '#') + return key.slice(1); + else + return this.scope + "." + key; + }; -ScopedTranslateCall.prototype.normalizeKey = function(key) { - if (key[0] === '#') - return key.slice(1); - else - return this.scope + "." + key; -}; + ScopedTranslateCall.prototype.normalize = function() { + if (!this.inferredKey) this.key = this.normalizeKey(this.key); + TranslateCall.prototype.normalize.call(this); + }; -ScopedTranslateCall.prototype.normalize = function() { - if (!this.inferredKey) this.key = this.normalizeKey(this.key); - TranslateCall.prototype.normalize.call(this); -}; - -module.exports = ScopedTranslateCall; + return ScopedTranslateCall; +} diff --git a/gems/canvas_i18nliner/package.json b/gems/canvas_i18nliner/package.json index 837d8a0dd15..1e150fd35bb 100644 --- a/gems/canvas_i18nliner/package.json +++ b/gems/canvas_i18nliner/package.json @@ -5,7 +5,9 @@ "main": "./js/main", "version": "0.0.1", "dependencies": { - "i18nliner": "0.0.14", + "handlebars": "1.3.0", + "i18nliner": "0.0.15", + "i18nliner-handlebars": "0.0.11", "minimist": "^1.1.0" } } diff --git a/gems/handlebars_tasks/lib/handlebars_tasks/handlebars.rb b/gems/handlebars_tasks/lib/handlebars_tasks/handlebars.rb index a3b985cc55c..d613d78f2cf 100644 --- a/gems/handlebars_tasks/lib/handlebars_tasks/handlebars.rb +++ b/gems/handlebars_tasks/lib/handlebars_tasks/handlebars.rb @@ -41,7 +41,6 @@ module HandlebarsTasks # compiled_path - See `compile` # plugin - See `compile` def compile_file(file, root_path, compiled_path, plugin=nil) - require 'execjs' id = file.gsub(root_path + '/', '').gsub(/.handlebars$/, '') path = "#{compiled_path}/#{id}.js" dir = File.dirname(path) @@ -53,7 +52,6 @@ module HandlebarsTasks end def compile_template(source, id, plugin=nil) - require 'execjs' # if the first letter of the template name is "_", register it as a partial # ex: _foobar.handlebars or subfolder/_something.handlebars filename = File.basename(id) @@ -71,12 +69,8 @@ module HandlebarsTasks css_registration = "\narguments[1]('#{id}', #{MultiJson.dump css});\n" end - scope = scopify(id) - prepared = prepare_i18n(source, scope) - dependencies << "i18n!#{scope}" if prepared[:keys].size > 0 - # take care of `require`ing partials - partials = find_partial_deps(prepared[:content]) + partials = find_partial_deps(source) partials.each do |partial| split = partial.split /\// split[-1] = "_#{split[-1]}" @@ -84,11 +78,13 @@ module HandlebarsTasks dependencies << "jst/#{require_path}" end - template = context.call "Handlebars.precompile", prepared[:content] + data = prepare_template(id, source) + dependencies << "i18n!#{data["scope"]}" if data["translationCount"] > 0 + <<-JS define('#{plugin ? plugin + "/" : ""}jst/#{id}', #{MultiJson.dump dependencies}, function (Handlebars) { var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; - templates['#{id}'] = template(#{template}); + templates['#{id}'] = template(#{data["template"]}); #{partial_registration} #{css_registration} return templates['#{id}']; @@ -96,22 +92,13 @@ define('#{plugin ? plugin + "/" : ""}jst/#{id}', #{MultiJson.dump dependencies}, JS end - # change a partial path into an i18n scope - # e.g. "fooBar/_lolz" -> "foo_bar.lolz" - def scopify(id) - # String#underscore may not be available - id.sub(/^_/, '').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase.gsub(/\/_?/, '.') - end - - def prepare_i18n(source, scope) - @extractor ||= I18nExtraction::HandlebarsExtractor.new - keys = [] - content = @extractor.scan(source, :method => :gsub) do |data| - wrappers = data[:wrappers].map{ |value, delimiter| " w#{delimiter.size-1}=#{value.inspect}" }.join - keys << data[:key] - "{{{t #{data[:key].inspect} #{data[:value].inspect} scope=#{scope.inspect}#{wrappers}#{data[:options]}}}}" - end - {:content => content, :keys => keys} + def prepare_template(path, source) + require 'json' + payload = {path: path, source: source}.to_json + compiler.puts payload + result = JSON.parse(compiler.readline) + raise result["error"] if result["error"] + result end def get_css(file_path) @@ -127,21 +114,15 @@ define('#{plugin ? plugin + "/" : ""}jst/#{id}', #{MultiJson.dump dependencies}, protected - # Returns the JavaScript context - def context - @context ||= self.set_context + # Returns the HBS preprocessor/compiler + def compiler + Thread.current[:hbs_compiler] ||= IO.popen("./gems/canvas_i18nliner/bin/prepare_hbs", "r+") end def find_partial_deps(template) # finds partials like: {{>foo bar}} and {{>[foo/bar] baz}} template.scan(/\{\{>\s?\[?(.+?)\]?( .*?)?}}/).map {|m| m[0].strip }.uniq end - - # Compiles and caches the handlebars JavaScript - def set_context - handlebars_source = File.read('public/javascripts/bower/handlebars/handlebars.js') - @context = ExecJS.compile handlebars_source - end end end -end \ No newline at end of file +end diff --git a/gems/handlebars_tasks/spec/handlebars_tasks/handlebars_spec.rb b/gems/handlebars_tasks/spec/handlebars_tasks/handlebars_spec.rb deleted file mode 100644 index d77f99e9543..00000000000 --- a/gems/handlebars_tasks/spec/handlebars_tasks/handlebars_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -# encoding: UTF-8 -# -# Copyright (C) 2011 Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . -# - -require 'spec_helper' - -module HandlebarsTasks - - describe Handlebars do - - context "i18n" do - - it "should convert translate helper blocks to inline calls" do - Handlebars.prepare_i18n('{{#t "test"}}this is a test of {{foo}}{{/t}}', 'test')[:content]. - should eql('{{{t "test" "this is a test of %{foo}" scope="test"}}}') - end - - it "should flag triple-stashed interpolation variables as safe" do - Handlebars.prepare_i18n('{{#t "pizza"}}give me {{{input}}} pizzas{{/t}}', 'test')[:content]. - should eql('{{{t "pizza" "give me %h{input} pizzas" scope="test"}}}') - end - - it "should extract wrappers" do - Handlebars.prepare_i18n('{{#t "test"}}{{person}} is so cool{{/t}}', 'test')[:content]. - should eql('{{{t "test" "*%{person}* is *so* **cool**" scope="test" w0="$1" w1="$1"}}}') - end - - it "should remove extraneous whitespace from the translation and wrappers" do - Handlebars.prepare_i18n(<<-HBS, 'test')[:content].strip. - {{#t "test"}} - - ohai - - {{/t}} - HBS - should eql('{{{t "test" "*ohai *" scope="test" w0=" $1"}}}') - end - - it "should not allow nested helper calls" do - lambda { - Handlebars.prepare_i18n('{{#t "test"}}{{call a helper}}{{/t}}', 'test') - }.should raise_error - end - - it "should fix up the scope" do - Handlebars.scopify('_test').should == "test" - Handlebars.scopify('test/test').should == "test.test" - Handlebars.scopify('test/_this_is-a_test').should == "test.this_is_a_test" - Handlebars.scopify('test/_andThisIsATest').should == "test.and_this_is_a_test" - end - - end - - end -end \ No newline at end of file diff --git a/gems/i18n_extraction/lib/i18n_extraction/abstract_extractor.rb b/gems/i18n_extraction/lib/i18n_extraction/abstract_extractor.rb old mode 100755 new mode 100644 index a0e2a1a9f6e..47caa2683a5 --- a/gems/i18n_extraction/lib/i18n_extraction/abstract_extractor.rb +++ b/gems/i18n_extraction/lib/i18n_extraction/abstract_extractor.rb @@ -57,4 +57,4 @@ module I18nExtraction end end end -end \ No newline at end of file +end diff --git a/gems/i18n_extraction/lib/i18n_extraction/i18nliner_extensions.rb b/gems/i18n_extraction/lib/i18n_extraction/i18nliner_extensions.rb index c66307d29ed..c4df468f913 100644 --- a/gems/i18n_extraction/lib/i18n_extraction/i18nliner_extensions.rb +++ b/gems/i18n_extraction/lib/i18n_extraction/i18nliner_extensions.rb @@ -5,6 +5,8 @@ require "i18nliner/processors/erb_processor" require "i18nliner/errors" require_relative "i18nliner_scope_extensions" +require "active_support/core_ext/module/aliasing" + module I18nliner class HtmlTagsInDefaultTranslationError < ExtractionError; end class AmbiguousTranslationKeyError < ExtractionError; end diff --git a/gems/i18n_tasks/lib/tasks/i18n.rake b/gems/i18n_tasks/lib/tasks/i18n.rake index 74fff053714..ee37c762e0f 100755 --- a/gems/i18n_tasks/lib/tasks/i18n.rake +++ b/gems/i18n_tasks/lib/tasks/i18n.rake @@ -2,59 +2,10 @@ require 'i18n_tasks' require 'i18n_extraction' namespace :i18n do - def infer_scope(filename) - case filename - when /app\/views\/.*\.handlebars\z/ - filename.gsub(/.*app\/views\/jst\/_?|\.handlebars\z/, '').gsub(/plugins\/([^\/]*)\//, '').underscore.gsub(/\/_?/, '.') - else - '' - end - end - desc "Verifies all translation calls" task :check => :environment do Hash.send(:include, I18nTasks::HashExtensions) unless Hash.new.kind_of?(I18nTasks::HashExtensions) - require 'ya2yaml' - - only = if ENV['ONLY'] - ENV['ONLY'].split(',').map{ |path| - path = '**/' + path if path =~ /\*/ - path = './' + path unless path =~ /\A.?\// - if path =~ /\*/ - path = Dir.glob(path) - elsif path !~ /\.(e?rb|js)\z/ - path = Dir.glob(path + '/**/*') - end - path - }.flatten - end - - COLOR_ENABLED = ($stdout.tty? rescue false) - def color(text, color_code) - COLOR_ENABLED ? "#{color_code}#{text}\e[0m" : text - end - - def green(text) - color(text, "\e[32m") - end - - def red(text) - color(text, "\e[31m") - end - - @errors = [] - def process_files(files) - files.each do |file| - begin - print green "." if yield file - rescue SyntaxError, StandardError - @errors << "#{$!}\n#{file}" - print red "F" - end - end - end - I18n.available_locales def I18nliner.manual_translations @@ -62,10 +13,10 @@ namespace :i18n do end - puts "\nJS..." + puts "\nJS/HBS..." system "./gems/canvas_i18nliner/bin/i18nliner export" if $?.exitstatus > 0 - $stderr.puts "Error extracting JS translations; confirm that `./gems/canvas_i18nliner/bin/i18nliner export` works" + $stderr.puts "Error extracting JS/HBS translations; confirm that `./gems/canvas_i18nliner/bin/i18nliner export` works" exit $?.exitstatus end js_translations = JSON.parse(File.read("config/locales/generated/en.json"))["en"].flatten_keys @@ -73,47 +24,21 @@ namespace :i18n do puts "\nRuby..." require 'i18nliner/commands/check' - options = {:only => ENV['ONLY']} @command = I18nliner::Commands::Check.run(options) @command.success? or exit 1 - total_ruby = @command.processors.sum(&:translation_count) @translations = @command.translations # merge js in js_translations.each do |key, value| @translations[key] = value end - - t = Time.now - - - puts "\nHandlebars..." - file_count = 0 - files = Dir.glob('./app/views/jst/{,**/*/**/}*.handlebars') - files &= only if only - handlebars_extractor = I18nExtraction::HandlebarsExtractor.new(:translations => @translations) - process_files(files) do |file| - file_count += 1 if handlebars_extractor.process(File.read(file), infer_scope(file)) - end - - print "\n\n" - failure = @errors.size > 0 - - @errors.each_index do |i| - puts "#{i+1})" - puts red @errors[i] - print "\n" - end - - print "Finished in #{Time.now - t} seconds\n\n" - total_strings = handlebars_extractor.total_unique - puts send((failure ? :red : :green), "#{file_count} files, #{total_strings} strings, #{@errors.size} failures") - raise "check command encountered errors" if failure end desc "Generates a new en.yml file for all translations" task :generate => :check do + require 'ya2yaml' + yaml_dir = './config/locales/generated' FileUtils.mkdir_p(File.join(yaml_dir)) yaml_file = File.join(yaml_dir, "en.yml") @@ -149,8 +74,6 @@ namespace :i18n do Hash.send(:include, I18nTasks::HashExtensions) unless Hash.new.kind_of?(I18nTasks::HashExtensions) - file_translations = {} - locales = I18n.available_locales - [:en] # allow passing of extra, empty locales by including a comma-separated # list of abbreviations in the LOCALES environment variable. e.g. @@ -165,54 +88,12 @@ namespace :i18n do exit 0 end - add_translations = lambda do |scope, translations| - file_translations[scope] ||= {} - locales.each do |locale| - file_translations[scope].update flat_translations.slice(*translations.map{ |k| k.gsub(/\A/, "#{locale}.") }) - end - end - - # Process a single file - process_file = lambda do |extractor, filename, arg_block| - extractor.translations = {} - - begin - unless extractor.process(File.read(filename), *arg_block.call(filename)) - return - end - rescue Exception => e - puts e - raise "Error reading #{file}: #{$!}\nYou should probably run `rake i18n:check' first" - end - - translations = extractor.translations.flatten_keys.keys - - unless translations.empty? - add_translations.call(extractor.scope, translations) - end - end - - process_files = lambda do |extractor, files, arg_block| - files.each do |filename| - process_file.call(extractor, filename, arg_block) - end - end - - # JavaScript system "./gems/canvas_i18nliner/bin/i18nliner generate_js" if $?.exitstatus > 0 $stderr.puts "Error extracting JS translations; confirm that `./gems/canvas_i18nliner/bin/i18nliner generate_js` works" exit $?.exitstatus end - js_scope_key_map = JSON.parse(File.read("config/locales/generated/js_bundles.json")) - js_scope_key_map.each do |scope, keys| - add_translations.call(scope, keys) - end - - # Handlebars - files = Dir.glob('./app/views/jst/{,**/*/**/}*.handlebars') - handlebars_extractor = I18nExtraction::HandlebarsExtractor.new - process_files.call(handlebars_extractor, files, lambda{ |file| [infer_scope(file)] }) + file_translations = JSON.parse(File.read("config/locales/generated/js_bundles.json")) dump_translations = lambda do |translation_name, translations| file = "public/javascripts/translations/#{translation_name}.js" @@ -222,7 +103,11 @@ namespace :i18n do end end - file_translations.each do |scope, translations| + file_translations.each do |scope, keys| + translations = {} + locales.each do |locale| + translations.update flat_translations.slice(*keys.map{ |k| k.gsub(/\A/, "#{locale}.") }) + end dump_translations.call(scope, translations.expand_keys) end diff --git a/public/javascripts/i18nObj.js b/public/javascripts/i18nObj.js index a9f4fd6f2cd..f733e37af6f 100644 --- a/public/javascripts/i18nObj.js +++ b/public/javascripts/i18nObj.js @@ -8,16 +8,6 @@ define([ I18n.locale = document.documentElement.getAttribute('lang'); -I18n.isValidNode = function(obj, node) { - // handle names like "foo.bar.baz" - var nameParts = node.split('.'); - for (var j=0; j < nameParts.length; j++) { - obj = obj[nameParts[j]]; - if (typeof obj === 'undefined' || obj === null) return false; - } - return true; -}; - I18n.lookup = function(scope, options) { var translations = this.prepareOptions(I18n.translations); var locales = [I18n.currentLocale()]; @@ -51,39 +41,6 @@ I18n.lookup = function(scope, options) { return messages; }; -// 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); - var matches = message.match(this.PLACEHOLDER); - - if (!matches) { - return message; - } - - var placeholder, value, name; - - for (var i = 0; placeholder = matches[i]; i++) { - name = placeholder.replace(this.PLACEHOLDER, "$1"); - - // handle names like "foo.bar.baz" - var nameParts = name.split('.'); - value = options; - for (var j=0; j < nameParts.length; j++) { - value = value[nameParts[j]]; - } - - if (!this.isValidNode(options, name)) { - value = "[missing " + placeholder + " value]"; - } - - regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}")); - message = message.replace(regex, value); - } - - return message; -}; - var _localize = I18n.localize; I18n.localize = function(scope, value) { var result = _localize.call(this, scope, value); diff --git a/spec/selenium/handlebars_spec.rb b/spec/selenium/handlebars_spec.rb index c8384829090..d371411a5df 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 {{{raw_input}}}{{/t}}

    -

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

    +

    {{#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 "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,8 +51,6 @@ 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' @@ -128,7 +126,7 @@ describe "handlebars" do expect(run_template(template, {}, 'fr')).to eq <<-HTML

    - Je voudrais un croissant + Je voudrais un croissant

    Yes, that's true, he would