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:
parent
2d37c16193
commit
6d8bac05b4
|
@ -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}"
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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"];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -57,4 +57,4 @@ module I18nExtraction
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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's true, he would </i>
|
||||
|
|
Loading…
Reference in New Issue