i18nliner-handlebars (part I)

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 <jstern@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:
Jon Jensen 2014-10-17 16:41:07 -06:00
parent 2d37c16193
commit 6d8bac05b4
19 changed files with 220 additions and 317 deletions

View File

@ -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}<br>#{courseText}: #{courseDatetime}"

View File

@ -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: [
"<a href=\"#{terms_of_use_url}\" target=\"_blank\">$1</a>"
"<a href=\"#{privacy_policy_url}\" target=\"_blank\">$1</a>"
]
)
signupDialog = (id, title) ->
return unless templates[id]
$node = $nodes[id] ?= $('<div />')
$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()

View File

@ -29,7 +29,7 @@
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="user[terms_of_use]" value="1">
{{#t "agree_to_terms_and_pp"}}You agree to the <a href="{{terms_url}}" target="_blank">terms of use</a> and acknowledge the <a href="{{privacy_url}}" target="_blank">privacy policy</a>.{{/t}}
{{{terms_html}}}
</label>
</div>
</div>

View File

@ -35,7 +35,7 @@
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="user[terms_of_use]" value="1">
{{#t "agree_to_terms_and_pp"}}You agree to the <a href="{{terms_url}}" target="_blank">terms of use</a> and acknowledge the <a href="{{privacy_url}}" target="_blank">privacy policy</a>.{{/t}}
{{{terms_html}}}
</label>
</div>
</div>

View File

@ -17,7 +17,7 @@
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="user[terms_of_use]" value="1">
{{#t "agree_to_terms_and_pp"}}You agree to the <a href="{{terms_url}}" target="_blank">terms of use</a> and acknowledge the <a href="{{privacy_url}}" target="_blank">privacy policy</a>.{{/t}}
{{{terms_html}}}
</label>
</div>
</div>

View File

@ -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");
}
});

View File

@ -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 = {

View File

@ -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;

View File

@ -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;
}

View File

@ -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"];

View File

@ -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;
}

View File

@ -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"
}
}

View File

@ -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
end

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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"}}<b>{{person}}</b> is <b>so</b> <b title="{{definition}}"><i>cool</i></b>{{/t}}', 'test')[:content].
should eql('{{{t "test" "*%{person}* is *so* **cool**" scope="test" w0="<b>$1</b>" w1="<b title=\\"%{definition}\\"><i>$1</i></b>"}}}')
end
it "should remove extraneous whitespace from the translation and wrappers" do
Handlebars.prepare_i18n(<<-HBS, 'test')[:content].strip.
{{#t "test"}}
<b>
ohai
</b>
{{/t}}
HBS
should eql('{{{t "test" "*ohai *" scope="test" w0="<b> $1</b>"}}}')
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

View File

@ -57,4 +57,4 @@ module I18nExtraction
end
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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 {{{raw_input}}}{{/t}}</p>
<p>{{#t "reversed"}}in other words you get {{{raw_input}}} when you type {{input}}{{/t}}</p>
<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 "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,8 +51,6 @@ 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>'
@ -128,7 +126,7 @@ describe "handlebars" do
expect(run_template(template, {}, 'fr')).to eq <<-HTML
<p>
<b> Je voudrais un croissant</b>
<b>Je voudrais un croissant</b>
</p>
<p>
<i> Yes, that&#39;s true, he would </i>