extract i18n_extraction gem
fixes CNVS-11182 test plan: * rake i18n tasks work correctly * rake jst tasks work correctly Change-Id: I9777649e338d81cd7129c887acc18d9ef6722a92 Reviewed-on: https://gerrit.instructure.com/31440 Reviewed-by: Jon Jensen <jon@instructure.com> QA-Review: Clare Strong <clare@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Product-Review: Simon Williams <simon@instructure.com>
This commit is contained in:
parent
b6cbbdc0cd
commit
4fbe46509c
|
@ -1,6 +1,6 @@
|
|||
group :i18n_tools do
|
||||
gem 'ruby_parser', '3.1.3'
|
||||
gem 'sexp_processor', '4.2.1'
|
||||
gem 'ya2yaml', '0.30'
|
||||
|
||||
gem 'i18n_extraction', :path => 'gems/i18n_extraction', :require => false
|
||||
end
|
||||
|
||||
|
|
2
Rakefile
2
Rakefile
|
@ -14,8 +14,10 @@ require 'rake/testtask'
|
|||
require 'rdoc/task'
|
||||
|
||||
if CANVAS_RAILS2
|
||||
Dir["#{RAILS_ROOT}/gems/**/lib/tasks/*.rake"].sort.each { |ext| load ext }
|
||||
require 'tasks/rails'
|
||||
else
|
||||
Dir["#{Rails.root}/gems/**/lib/tasks/*.rake"].sort.each { |ext| load ext }
|
||||
CanvasRails::Application.load_tasks
|
||||
end
|
||||
begin; require 'parallelized_specs/lib/parallelized_specs/tasks'; rescue LoadError; end
|
||||
|
|
|
@ -8,11 +8,13 @@ bundle exec rspec spec
|
|||
let result=$result+$?
|
||||
|
||||
echo "################ Running tests against Rails 3 ################"
|
||||
rm -f Gemfile.lock
|
||||
mv Gemfile.lock Gemfile.lock.rails2
|
||||
export CANVAS_RAILS3=true
|
||||
bundle install
|
||||
bundle exec rspec spec
|
||||
let result=$result+$?
|
||||
mv Gemfile.lock.rails2 Gemfile.lock
|
||||
|
||||
|
||||
if [ $result -eq 0 ]; then
|
||||
echo "SUCCESS"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
--color
|
||||
--format progress
|
|
@ -0,0 +1,3 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
|
@ -0,0 +1 @@
|
|||
require "bundler/gem_tasks"
|
|
@ -0,0 +1,32 @@
|
|||
# coding: utf-8
|
||||
lib = File.expand_path('../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
|
||||
unless defined?(CANVAS_RAILS3)
|
||||
require File.expand_path("../../../config/canvas_rails3", __FILE__)
|
||||
end
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "i18n_extraction"
|
||||
spec.version = '0.0.1'
|
||||
spec.authors = ["Raphael Weiner"]
|
||||
spec.email = ["rweiner@pivotallabs.com"]
|
||||
spec.summary = %q{i18n extraction for Instructure}
|
||||
|
||||
spec.files = Dir.glob("{lib,spec}/**/*") + %w(Rakefile test.sh)
|
||||
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
||||
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_dependency "sexp_processor", "4.2.1"
|
||||
spec.add_dependency "ruby_parser", "3.1.3"
|
||||
if CANVAS_RAILS3
|
||||
spec.add_dependency "activesupport", "~> 3.2"
|
||||
else
|
||||
spec.add_dependency "activesupport", "~> 2.3"
|
||||
end
|
||||
|
||||
spec.add_development_dependency "bundler", "~> 1.5"
|
||||
spec.add_development_dependency "rake"
|
||||
spec.add_development_dependency "rspec"
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
require "sexp_processor"
|
||||
require "ruby_parser"
|
||||
require "json"
|
||||
require "active_support/all"
|
||||
|
||||
module I18nExtraction
|
||||
require "i18n_extraction/abstract_extractor"
|
||||
require "i18n_extraction/handlebars_extractor"
|
||||
require "i18n_extraction/js_extractor"
|
||||
require "i18n_extraction/ruby_extractor"
|
||||
end
|
|
@ -57,6 +57,4 @@ module I18nExtraction
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -1,5 +1,3 @@
|
|||
require 'lib/i18n_extraction/abstract_extractor'
|
||||
|
||||
module I18nExtraction
|
||||
class HandlebarsExtractor
|
||||
include AbstractExtractor
|
||||
|
@ -14,7 +12,7 @@ module I18nExtraction
|
|||
\}\}
|
||||
/x
|
||||
I18N_CALL = /
|
||||
#{I18N_CALL_START}
|
||||
#{I18N_CALL_START}
|
||||
(?<content> .*?)
|
||||
\{\{\/t\}\}
|
||||
/mx
|
||||
|
@ -39,7 +37,7 @@ module I18nExtraction
|
|||
|
||||
def scan(source, options={})
|
||||
options = {
|
||||
:method => :scan
|
||||
:method => :scan
|
||||
}.merge(options)
|
||||
|
||||
method = options[:method]
|
||||
|
@ -67,10 +65,10 @@ module I18nExtraction
|
|||
content.gsub!(/\s+/, ' ')
|
||||
content.strip!
|
||||
yield :key => key,
|
||||
:value => content,
|
||||
:options => opts,
|
||||
:wrappers => wrappers,
|
||||
:line_number => line_number
|
||||
:value => content,
|
||||
:options => opts,
|
||||
:wrappers => wrappers,
|
||||
:line_number => line_number
|
||||
end
|
||||
raise "possibly unterminated #t call (line #{block_line_numbers.shift} or earlier)" unless block_line_numbers.empty?
|
||||
result
|
||||
|
@ -107,7 +105,7 @@ module I18nExtraction
|
|||
|
||||
def balanced_tags?(open, close)
|
||||
open.scan(TAG_START).map { |tag| tag.match(TAG_NAME).to_s } ==
|
||||
close.scan(TAG_END).map { |tag| tag.match(TAG_NAME).to_s }.reverse
|
||||
close.scan(TAG_END).map { |tag| tag.match(TAG_NAME).to_s }.reverse
|
||||
end
|
||||
|
||||
def check_html(source, base_line_number)
|
|
@ -1,5 +1,3 @@
|
|||
require 'lib/i18n_extraction/abstract_extractor'
|
||||
|
||||
module I18nExtraction
|
||||
class JsExtractor
|
||||
include AbstractExtractor
|
||||
|
@ -55,8 +53,8 @@ module I18nExtraction
|
|||
I18N_CALL_START = /I18n\.(t|translate|beforeLabel)\(/
|
||||
I18N_KEY_OR_SIMPLE_EXPRESSION = /(#{I18N_KEY}|([\w\.]+|\(['"][\w.]+['"]\))+)/
|
||||
I18N_CALL = /
|
||||
#{I18N_CALL_START}
|
||||
#{I18N_KEY_OR_SIMPLE_EXPRESSION}
|
||||
#{I18N_CALL_START}
|
||||
#{I18N_KEY_OR_SIMPLE_EXPRESSION}
|
||||
(,\s*
|
||||
( #{STRING_CONCATENATION} | #{REALLY_SIMPLE_HASH_LITERAL} ) # default
|
||||
(,\s*
|
||||
|
@ -93,7 +91,7 @@ module I18nExtraction
|
|||
end
|
||||
end
|
||||
matches = []
|
||||
source.scan(full_pattern){ |args| matches << [$&] + args }
|
||||
source.scan(full_pattern) { |args| matches << [$&] + args }
|
||||
raise "expected/actual mismatch (probably a bug)" if expected.size < matches.size
|
||||
expected.each_index do |i|
|
||||
expected_string = expected[i].first.strip
|
||||
|
@ -280,4 +278,4 @@ module I18nExtraction
|
|||
instance_eval(string.gsub(/(^|[^\\])#/, '\1\\#'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,8 +1,7 @@
|
|||
require 'lib/i18n_extraction/abstract_extractor'
|
||||
|
||||
module I18nExtraction
|
||||
class RubyExtractor < SexpProcessor
|
||||
include AbstractExtractor
|
||||
|
||||
attr_accessor :in_html_view
|
||||
|
||||
def process_defn(exp)
|
||||
|
@ -69,27 +68,27 @@ module I18nExtraction
|
|||
default = process_default_translation(args.shift, key)
|
||||
|
||||
options = if args.first.is_a?(Sexp)
|
||||
if method == :jt
|
||||
if args.first.sexp_type != :str
|
||||
raise "jt options must be a javascript string: #{key.inspect} on line #{line}"
|
||||
end
|
||||
str = args.shift.last
|
||||
str.scan(/['"]?(\w+)['"]?:/).flatten.map(&:to_sym)
|
||||
else
|
||||
if args.first.sexp_type != :hash
|
||||
raise "translate options must be a hash: #{key.inspect} on line #{line}"
|
||||
end
|
||||
hash = args.shift
|
||||
hash.shift
|
||||
(0...(hash.size/2)).map{ |i|
|
||||
process hash[i * 2 + 1]
|
||||
raise "option keys must be strings or symbols on line #{line}" unless [:lit, :str].include?(hash[i * 2].sexp_type)
|
||||
hash[i * 2].last.to_sym
|
||||
}
|
||||
end
|
||||
else
|
||||
[]
|
||||
end
|
||||
if method == :jt
|
||||
if args.first.sexp_type != :str
|
||||
raise "jt options must be a javascript string: #{key.inspect} on line #{line}"
|
||||
end
|
||||
str = args.shift.last
|
||||
str.scan(/['"]?(\w+)['"]?:/).flatten.map(&:to_sym)
|
||||
else
|
||||
if args.first.sexp_type != :hash
|
||||
raise "translate options must be a hash: #{key.inspect} on line #{line}"
|
||||
end
|
||||
hash = args.shift
|
||||
hash.shift
|
||||
(0...(hash.size/2)).map { |i|
|
||||
process hash[i * 2 + 1]
|
||||
raise "option keys must be strings or symbols on line #{line}" unless [:lit, :str].include?(hash[i * 2].sexp_type)
|
||||
hash[i * 2].last.to_sym
|
||||
}
|
||||
end
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
# single word count/pluralization fu
|
||||
if default.is_a?(String) && default =~ /\A[\w\-]+\z/ && options.include?(:count)
|
||||
|
@ -121,12 +120,12 @@ module I18nExtraction
|
|||
inferred = false
|
||||
default = nil
|
||||
key_arg = if args.size == 1 || args[1] && args[1].is_a?(Sexp) && args[1].sexp_type == :hash
|
||||
inferred = true
|
||||
args.shift
|
||||
elsif args[1].is_a?(Sexp)
|
||||
args.shift
|
||||
args.shift
|
||||
end
|
||||
inferred = true
|
||||
args.shift
|
||||
elsif args[1].is_a?(Sexp)
|
||||
args.shift
|
||||
args.shift
|
||||
end
|
||||
if args.first.is_a?(Sexp) && args.first.sexp_type == :hash
|
||||
hash_args = args.shift
|
||||
hash_args.shift
|
||||
|
@ -175,18 +174,18 @@ module I18nExtraction
|
|||
raise "invalid en default #{exp.inspect}" unless exp.is_a?(Sexp)
|
||||
if exp.sexp_type == :hash
|
||||
exp.shift
|
||||
hash = Hash[*exp.map{ |e| process_possible_string_concat(e, :allow_symbols => true) }]
|
||||
hash = Hash[*exp.map { |e| process_possible_string_concat(e, :allow_symbols => true) }]
|
||||
pluralization_keys = hash.keys
|
||||
if (pluralization_keys - allowed_pluralization_keys).size > 0
|
||||
raise "invalid :count sub-key(s) #{exp.inspect} on line #{exp.line}"
|
||||
elsif required_pluralization_keys & pluralization_keys != required_pluralization_keys
|
||||
raise "not all required :count sub-key(s) provided on line #{exp.line} (expected #{required_pluralization_keys.join(', ')})"
|
||||
elsif hash.values.any?{ |v| !v.is_a?(String) }
|
||||
elsif hash.values.any? { |v| !v.is_a?(String) }
|
||||
raise "invalid en count default(s) #{exp.inspect} on line #{exp.line}"
|
||||
end
|
||||
hash
|
||||
else
|
||||
process_possible_string_concat(exp, :top_level_error => lambda{ |exp| "invalid en default #{exp.inspect} on line #{exp.line}" })
|
||||
process_possible_string_concat(exp, :top_level_error => lambda { |exp| "invalid en default #{exp.inspect} on line #{exp.line}" })
|
||||
end
|
||||
rescue
|
||||
raise "#{$!} (#{key.inspect})"
|
||||
|
@ -204,4 +203,4 @@ module I18nExtraction
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,109 @@
|
|||
#
|
||||
# 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 I18nExtraction
|
||||
|
||||
describe HandlebarsExtractor do
|
||||
def extract(source, scope = 'asdf', options = {})
|
||||
scope_results = scope && (options.has_key?(:scope_results) ? options.delete(:scope_results) : true)
|
||||
|
||||
extractor = HandlebarsExtractor.new
|
||||
extractor.process(source, scope)
|
||||
(scope_results ?
|
||||
scope.split(/\./).inject(extractor.translations) { |hash, s| hash[s] } :
|
||||
extractor.translations) || {}
|
||||
end
|
||||
|
||||
context "keys" do
|
||||
it "should allow valid string keys" do
|
||||
extract('{{#t "foo"}}Foo{{/t}}').should eql({'foo' => "Foo"})
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda { extract '{{#t "foo foo"}}Foo{{/t}}' }.should raise_error 'invalid translation key "foo foo" on line 1'
|
||||
end
|
||||
end
|
||||
|
||||
context "well-formed-ness" do
|
||||
it "should make sure all #t calls are closed" do
|
||||
lambda { extract "{{#t \"foo\"}}Foo{{/t}}\n{{#t \"bar\"}}...\nruh-roh\n" }.should raise_error /possibly unterminated #t call \(line 2/
|
||||
end
|
||||
end
|
||||
|
||||
context "values" do
|
||||
it "should strip extraneous whitespace" do
|
||||
extract("{{#t \"foo\"}}\t Foo\n foo\r\n\ffoo!!! {{/t}}").should eql({'foo' => 'Foo foo foo!!!'})
|
||||
end
|
||||
end
|
||||
|
||||
context "placeholders" do
|
||||
it "should allow simple placeholders" do
|
||||
extract('{{#t "foo"}}Hello {{user.name}}{{/t}}').should eql({'foo' => 'Hello %{user.name}'})
|
||||
end
|
||||
|
||||
it "should disallow helpers or anything else" do
|
||||
lambda { extract '{{#t "foo"}}Hello {{call a helper}}{{/t}}' }.should raise_error 'helpers may not be used inside #t calls (line 1)'
|
||||
end
|
||||
end
|
||||
|
||||
context "wrappers" do
|
||||
it "should infer wrappers" do
|
||||
extract('{{#t "foo"}}Be sure to <a href="{{url}}">log in</a>. <b>Don\'t</b> you <b>dare</b> forget!!!{{/t}}').should eql({'foo' => 'Be sure to *log in*. **Don\'t** you **dare** forget!!!'})
|
||||
end
|
||||
|
||||
it "should not infer wrappers from unbalanced tags" do
|
||||
lambda { extract '{{#t "foo"}}you are <b><i>so cool</i></strong>{{/t}}' }.should raise_error 'translation contains un-wrapper-ed markup (line 1). hint: use a placeholder, or balance your markup'
|
||||
end
|
||||
|
||||
it "should allow empty tags on either side of the wrapper" do
|
||||
extract('{{#t "bar"}}you can <button><i class="icon-email"></i>send an email</button>{{/t}}').should eql({'bar' => 'you can *send an email*'})
|
||||
extract('{{#t "baz"}}this is <b>so cool!<img /></b>{{/t}}').should eql({'baz' => 'this is *so cool!*'})
|
||||
end
|
||||
|
||||
it "should disallow any un-wrapper-ed html" do
|
||||
lambda { extract '{{#t "foo"}}check out this pic: <img src="pic.gif">{{/t}}' }.should raise_error 'translation contains un-wrapper-ed markup (line 1). hint: use a placeholder, or balance your markup'
|
||||
end
|
||||
end
|
||||
|
||||
context "scoping" do
|
||||
it "should auto-scope relative keys to the current scope" do
|
||||
extract('{{#t "foo"}}Foo{{/t}}', 'asdf', :scope_results => false).should eql({'asdf' => {'foo' => "Foo"}})
|
||||
end
|
||||
|
||||
it "should not auto-scope absolute keys" do
|
||||
extract('{{#t "#foo"}}Foo{{/t}}', 'asdf', :scope_results => false).should eql({'foo' => "Foo"})
|
||||
end
|
||||
end
|
||||
|
||||
context "collisions" do
|
||||
it "should not let you reuse a key" do
|
||||
lambda { extract '{{#t "foo"}}Foo{{/t}}{{#t "foo"}}foo{{/t}}' }.should raise_error 'cannot reuse key "asdf.foo"'
|
||||
end
|
||||
|
||||
it "should not let you use a scope as a key" do
|
||||
lambda { extract '{{#t "foo.bar"}}bar{{/t}}{{#t "foo"}}foo{{/t}}' }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
|
||||
it "should not let you use a key as a scope" do
|
||||
lambda { extract '{{#t "foo"}}foo{{/t}}{{#t "foo.bar"}}bar{{/t}}' }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,208 @@
|
|||
#
|
||||
# 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 I18nExtraction
|
||||
|
||||
describe JsExtractor do
|
||||
def extract(source, scope = 'asdf', options = {})
|
||||
scope_results = scope && (options.has_key?(:scope_results) ? options.delete(:scope_results) : true)
|
||||
|
||||
extractor = JsExtractor.new
|
||||
source = "require(['i18n!#{scope}'], function(I18n) {\n#{source.gsub(/^/, ' ')}\n});" if scope
|
||||
extractor.process(source, options)
|
||||
(scope_results ?
|
||||
scope.split(/\./).inject(extractor.translations) { |hash, s| hash[s] } :
|
||||
extractor.translations) || {}
|
||||
end
|
||||
|
||||
context "keys" do
|
||||
it "should allow valid string keys" do
|
||||
extract("I18n.t('foo', \"Foo\")").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda { extract "I18n.t(foo, \"Foo\")" }.should raise_error /invalid key/
|
||||
lambda { extract "I18n.t('f' + 'o' + 'o', \"Foo\"" }.should raise_error /unable to "parse" I18n call/
|
||||
lambda { extract "I18n.t('f o o', \"Foo\"" }.should raise_error /unable to "parse" I18n call/
|
||||
end
|
||||
end
|
||||
|
||||
context "default translations" do
|
||||
it "should allow strings" do
|
||||
extract("I18n.t('foo', \"Foo\")").should == {'foo' => "Foo"}
|
||||
extract("I18n.t('foo', 'F' +\n'o' +\n'o')").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda { extract "I18n.t('foo', foo)" }.should raise_error /unable to "parse" I18n call/
|
||||
lambda { extract "I18n.t('foo', I18n.t('bar', 'bar'))" }.should raise_error /unable to "parse" I18n call/
|
||||
end
|
||||
|
||||
it "should complain if there is no default" do
|
||||
lambda { extract("I18n.t('foo')") }.should raise_error(/no default provided for "asdf.foo"/)
|
||||
end
|
||||
|
||||
it "should skip t for core translations with no defaults" do
|
||||
extract("I18n.t('#date.month_names')", 'asdf', :scope_results => false).should == {}
|
||||
end
|
||||
end
|
||||
|
||||
context "placeholders" do
|
||||
it "should ensure all placeholders have corresponding options" do
|
||||
lambda { extract "I18n.t('foo', 'i have a %{foo}')" }.should raise_error(/interpolation value not provided for :foo/)
|
||||
end
|
||||
end
|
||||
|
||||
context "pluralization" do
|
||||
it "should auto-pluralize single words + :count" do
|
||||
extract("I18n.t('foo', 'Foo', {count: 1})").should == {'foo' => {'one' => "1 Foo", 'other' => "%{count} Foos"}}
|
||||
end
|
||||
|
||||
it "should not auto-pluralize other strings + :count" do
|
||||
extract("I18n.t('foo', 'Foo foo', {count: 1})").should == {'foo' => "Foo foo"}
|
||||
end
|
||||
|
||||
it "should allow valid pluralization sub-keys" do
|
||||
extract("I18n.t('foo', {one: 'a foo', other: 'some foos'}, {count: 1})").should == {'foo' => {'one' => 'a foo', 'other' => 'some foos'}}
|
||||
end
|
||||
|
||||
it "should complain if not all required pluralization sub-keys are provided" do
|
||||
lambda { extract("I18n.t('foo', {other: 'some foos'}, {count: 1})") }.should raise_error(/not all required :count sub-key\(s\) provided/)
|
||||
end
|
||||
|
||||
it "should reject invalid pluralization sub-keys" do
|
||||
lambda { extract "I18n.t('foo', {invalid: '%{count} Foo'}, {count: 1})" }.should raise_error(/invalid :count sub-key\(s\):/)
|
||||
end
|
||||
end
|
||||
|
||||
context "nesting" do
|
||||
it "should correctly evaluate i18n calls that are arguments to other (possibly) i18n calls" do
|
||||
extract("I18n.t('foo', 'Foo said \"%{bar}\"', {bar: I18n.t('bar', 'Hello bar')})").should == {'foo' => 'Foo said "%{bar}"', 'bar' => 'Hello bar'}
|
||||
end
|
||||
end
|
||||
|
||||
context "scoping" do
|
||||
it "should correctly infer the scope" do
|
||||
extract(<<-SOURCE, nil).should == {'asdf' => {'bar' => 'Bar'}}
|
||||
define('bar',
|
||||
['foo',
|
||||
'i18n!asdf'
|
||||
], function(Foo, I18n) {
|
||||
I18n.t('bar', 'Bar');
|
||||
});
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should require a scope for all I18n calls" do
|
||||
lambda { extract(<<-SOURCE, nil) }.should raise_error /possibly unscoped I18n call on line 2/
|
||||
require(['i18n'], function(I18n) {
|
||||
I18n.t('bar', 'Bar');
|
||||
});
|
||||
SOURCE
|
||||
|
||||
lambda { extract(<<-SOURCE, nil) }.should raise_error /possibly unscoped I18n call on line 1/
|
||||
I18n.t('bar', 'Bar');
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should auto-scope relative keys to the current scope" do
|
||||
extract("I18n.t('foo', 'Foo')", 'asdf', :scope_results => false).should == {'asdf' => {'foo' => "Foo"}}
|
||||
end
|
||||
|
||||
it "should not auto-scope absolute keys" do
|
||||
extract("I18n.t('#foo', 'Foo')", 'asdf', :scope_results => false).should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should not allow multiple scopes" do
|
||||
lambda { extract(<<-SOURCE, nil) }.should raise_error /multiple scopes are not allowed/
|
||||
require(['i18n!asdf'], function(I18n) {
|
||||
I18n.t('bar', 'Bar');
|
||||
});
|
||||
require(['i18n!qwerty'], function(I18n) {
|
||||
I18n.t('lol', 'Wut');
|
||||
});
|
||||
SOURCE
|
||||
end
|
||||
end
|
||||
|
||||
context "collisions" do
|
||||
it "should not let you reuse a key" do
|
||||
lambda { extract "I18n.t('foo', 'Foo')\nI18n.t('foo', 'foo')" }.should raise_error 'cannot reuse key "asdf.foo"'
|
||||
end
|
||||
|
||||
it "should not let you use a scope as a key" do
|
||||
lambda { extract "I18n.t('foo.bar', 'bar')\nI18n.t('foo', 'foo')" }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
|
||||
it "should not let you use a key as a scope" do
|
||||
lambda { extract "I18n.t('foo', 'foo')\nI18n.t('foo.bar', 'bar')" }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
end
|
||||
|
||||
context "erb" do
|
||||
it "should support jt and I18n.l calls" do
|
||||
extract(<<-SOURCE, nil, :erb => true).should eql({}) # doesn't actually extract it
|
||||
<% js_block do %>
|
||||
<script>
|
||||
require(['i18n'], function(I18n) {
|
||||
I18n.l('asdf', 'asdf')
|
||||
<%= jt('bar', 'Bar') %>
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should not support the i18n AMD plugin" do
|
||||
lambda { extract(<<-SOURCE, nil, :erb => true) }.should raise_error "i18n amd plugin is not supported in js_blocks (line 3)"
|
||||
<% js_block do %>
|
||||
<script>
|
||||
require(['i18n!scope'], function(I18n) {
|
||||
<%= jt('bar', 'Bar') %>
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should not allow jt calls outside of a require/define block" do
|
||||
lambda { extract(<<-SOURCE, nil, :erb => true) }.should raise_error /possibly unscoped jt call on line 3/
|
||||
<% js_block do %>
|
||||
<script>
|
||||
<%= jt('bar', 'Bar') %>
|
||||
</script>
|
||||
<% end %>
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should not allow raw I18n calls" do
|
||||
lambda { extract(<<-SOURCE, nil, :erb => true) }.should raise_error /raw I18n call on line 4/
|
||||
<% js_block do %>
|
||||
<script>
|
||||
require(['i18n'], function(I18n) {
|
||||
I18n.t('bar', 'Bar');
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
||||
SOURCE
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,211 @@
|
|||
#
|
||||
# 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 I18nExtraction
|
||||
|
||||
describe RubyExtractor do
|
||||
def extract(source, scope = 'asdf.', scope_results = true, in_html_view = false)
|
||||
sexps = RubyParser.new.parse(source)
|
||||
extractor = RubyExtractor.new
|
||||
extractor.scope = scope
|
||||
extractor.in_html_view = in_html_view
|
||||
extractor.process(sexps)
|
||||
(scope_results ?
|
||||
scope.split(/\./).inject(extractor.translations) { |hash, s| hash[s] } :
|
||||
extractor.translations) || {}
|
||||
end
|
||||
|
||||
context "well-formedness" do
|
||||
it "should complain about missing arguments" do
|
||||
lambda { extract("t 'foo'") }.should raise_error /insufficient arguments for translate call/
|
||||
end
|
||||
end
|
||||
|
||||
context "keys" do
|
||||
it "should allow string keys" do
|
||||
extract("t 'foo', \"Foo\"").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should allow symbol keys" do
|
||||
extract("t :foo, \"Foo\"").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda { extract "t foo, \"Foo\"" }.should raise_error /invalid translation key/
|
||||
lambda { extract "t \"f\#{o}o\", \"Foo\"" }.should raise_error /invalid translation key/
|
||||
lambda { extract "t true ? :foo : :bar, \"Foo\"" }.should raise_error /invalid translation key/
|
||||
end
|
||||
end
|
||||
|
||||
context "default translations" do
|
||||
it "should allow strings" do
|
||||
extract("t 'foo', \"Foo\"").should == {'foo' => "Foo"}
|
||||
extract("t 'foo2', <<-STR\nFoo\nSTR").should == {'foo2' => "Foo"}
|
||||
extract("t 'foo', 'F' + 'o' + 'o'").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda { extract "t 'foo', \"F\#{o}o\"" }.should raise_error /invalid en default/
|
||||
lambda { extract "t 'foo', foo" }.should raise_error /invalid en default/
|
||||
lambda { extract "t 'foo', (true ? 'Foo' : 'Bar')" }.should raise_error /invalid en default/
|
||||
end
|
||||
end
|
||||
|
||||
context "placeholders" do
|
||||
it "should ensure all placeholders have corresponding options" do
|
||||
lambda { extract "t 'foo', 'i have a %{foo}'" }.should raise_error(/interpolation value not provided for :foo/)
|
||||
extract("t 'foo', 'i have a %{foo}', :foo => 'foo'").should == {'foo' => 'i have a %{foo}'}
|
||||
lambda { extract "jt 'foo', 'i have a %{foo}', '{}'" }.should raise_error(/interpolation value not provided for :foo/)
|
||||
extract("jt 'foo', 'i have a %{foo}', '{foo: a_foo}'").should == {'foo' => 'i have a %{foo}'}
|
||||
end
|
||||
end
|
||||
|
||||
context "pluralization" do
|
||||
it "should auto-pluralize single words + :count" do
|
||||
extract("t 'foo', 'Foo', :count => 1").should == {'foo' => {'one' => "1 Foo", 'other' => "%{count} Foos"}}
|
||||
end
|
||||
|
||||
it "should not auto-pluralize other strings + :count" do
|
||||
extract("t 'foo', 'Foo foo', :count => 1").should == {'foo' => "Foo foo"}
|
||||
end
|
||||
|
||||
it "should allow valid pluralization sub-keys" do
|
||||
extract("t 'foo', {:one => 'a foo', :other => 'some foos'}, :count => 1").should == {'foo' => {'one' => 'a foo', 'other' => 'some foos'}}
|
||||
end
|
||||
|
||||
it "should complain if not all required pluralization sub-keys are provided" do
|
||||
lambda { extract("t 'foo', {:other => 'some foos'}, :count => 1") }.should raise_error(/not all required :count sub-key\(s\) provided/)
|
||||
end
|
||||
|
||||
it "should reject invalid pluralization sub-keys" do
|
||||
lambda { extract "t 'foo', {:invalid => '%{count} Foo'}, :count => 1" }.should raise_error(/invalid :count sub-key\(s\)/)
|
||||
end
|
||||
end
|
||||
|
||||
context "labels" do
|
||||
it "should interpret symbol names as the key" do
|
||||
extract("label :thing, :the_foo, :foo, :en => 'Foo'").should == {'labels' => {"foo" => "Foo"}}
|
||||
extract("f.label :the_foo, :foo, :en => 'Foo'").should == {'labels' => {"foo" => "Foo"}}
|
||||
extract("label_tag :the_foo, :foo, :en => 'Foo'").should == {'labels' => {"foo" => "Foo"}}
|
||||
end
|
||||
|
||||
it "should infer the key from the method if not provided" do
|
||||
extract("label :thing, :the_foo, :en => 'Foo'").should == {'labels' => {"the_foo" => "Foo"}}
|
||||
extract("f.label :the_foo, :en => 'Foo'").should == {'labels' => {"the_foo" => "Foo"}}
|
||||
extract("label_tag :the_foo, :en => 'Foo'").should == {'labels' => {"the_foo" => "Foo"}}
|
||||
end
|
||||
|
||||
it "should skip label calls with non-symbol keys (i.e. just a standard label)" do
|
||||
extract("label :thing, :the_foo, 'foo'").should == {}
|
||||
extract("f.label :the_foo, 'foo'").should == {}
|
||||
extract("label_tag :thing, 'foo'").should == {}
|
||||
end
|
||||
|
||||
it "should complain if a label call has a non-symbol key and a default" do
|
||||
lambda { extract "label :thing, :the_foo, 'foo', :en => 'Foo'" }.should raise_error /invalid translation key/
|
||||
lambda { extract "f.label :the_foo, 'foo', :en => 'Foo'" }.should raise_error /invalid translation key/
|
||||
lambda { extract "label_tag :the_foo, 'foo', :en => 'Foo'" }.should raise_error /invalid translation key/
|
||||
end
|
||||
|
||||
it "should not auto-scope absolute keys" do
|
||||
extract("label :thing, :the_foo, :'#foo', :en => 'Foo'", '').should == {"foo" => "Foo"}
|
||||
extract("f.label :the_foo, :'#foo', :en => 'Foo'", '').should == {"foo" => "Foo"}
|
||||
extract("label_tag :the_foo, :'#foo', :en => 'Foo'", '').should == {"foo" => "Foo"}
|
||||
end
|
||||
|
||||
it "should complain if no default is provided" do
|
||||
lambda { extract "label :thing, :the_foo, :foo" }.should raise_error /invalid\/missing en default nil/
|
||||
lambda { extract "f.label :the_foo, :foo" }.should raise_error /invalid\/missing en default nil/
|
||||
lambda { extract "label_tag :the_foo, :foo" }.should raise_error /invalid\/missing en default nil/
|
||||
end
|
||||
end
|
||||
|
||||
context "nesting" do
|
||||
it "should correctly evaluate i18n calls that are arguments to other (possibly) i18n calls" do
|
||||
extract("t :foo, 'Foo said \"%{bar}\"', :bar => t(:bar, 'Hello bar')").should == {'foo' => 'Foo said "%{bar}"', 'bar' => 'Hello bar'}
|
||||
extract("label :thing, :the_foo, t(:bar, 'Bar')").should == {'bar' => 'Bar'}
|
||||
extract("f.label :the_foo, t(:bar, 'Bar')").should == {'bar' => 'Bar'}
|
||||
end
|
||||
|
||||
it "should ignore i18n calls within i18n method definitions" do
|
||||
extract("def t(*args); other.t *args; end").should == {}
|
||||
end
|
||||
end
|
||||
|
||||
context "scoping" do
|
||||
it "should auto-scope relative keys to the current scope" do
|
||||
extract("t 'foo', 'Foo'", 'asdf.', false).should == {'asdf' => {'foo' => "Foo"}}
|
||||
end
|
||||
|
||||
it "should not auto-scope absolute keys" do
|
||||
extract("t '#foo', 'Foo'", 'asdf.', false).should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should not auto-scope keys with I18n as the receiver" do
|
||||
extract("I18n.t 'foo', 'Foo'", 'asdf.', false).should == {'foo' => 'Foo'}
|
||||
end
|
||||
|
||||
it "should auto-scope plugin registration" do
|
||||
extract("Canvas::Plugin.register('dim_dim', :web_conferencing, {:name => lambda{ t :name, \"DimDim\" }})", '', false).should ==
|
||||
{'plugins' => {'dim_dim' => {'name' => "DimDim"}}}
|
||||
end
|
||||
|
||||
it "should require explicit keys if there is no scope" do
|
||||
lambda { extract("t 'foo', 'Foo'", '') }.should raise_error /ambiguous translation key "foo"/
|
||||
end
|
||||
end
|
||||
|
||||
context "collisions" do
|
||||
it "should not let you reuse a key" do
|
||||
lambda { extract "t 'foo', 'Foo'\nt 'foo', 'foo'" }.should raise_error 'cannot reuse key "asdf.foo"'
|
||||
end
|
||||
|
||||
it "should not let you use a scope as a key" do
|
||||
lambda { extract "t 'foo.bar', 'bar'\nt 'foo', 'foo'" }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
|
||||
it "should not let you use a key as a scope" do
|
||||
lambda { extract "t 'foo', 'foo'\nt 'foo.bar', 'bar'" }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
end
|
||||
|
||||
context "whitespace" do
|
||||
it "should remove extraneous whitespace from view translate calls" do
|
||||
extract("t 'foo', \"\\n Foo \\t foo!\\n\"", 'asdf.', true, true).should == {'foo' => "Foo foo!"}
|
||||
end
|
||||
|
||||
it "should strip whitespace from all other calls" do
|
||||
extract("t 'foo', \"\\n Foo \\t foo!\\n\"").should == {'foo' => "Foo \t foo!"}
|
||||
extract("mt 'foo', \"\\n Foo \\t foo!\\n\"").should == {'foo' => "Foo \t foo!"}
|
||||
end
|
||||
end
|
||||
|
||||
context "sanitization" do
|
||||
it "should reject stuff that looks sufficiently html-y" do
|
||||
lambda { extract "t 'dude', 'this is <em>important</em>'" }.should raise_error /html tags on line 1/
|
||||
end
|
||||
|
||||
it "should generally be ok with angle brackets" do
|
||||
extract("t 'obvious', 'TIL 1 < 2'").should == {'obvious' => 'TIL 1 < 2'}
|
||||
extract("t 'email', 'please enter an email, e.g. Joe User <joe@example.com>'").should == {'email' => 'please enter an email, e.g. Joe User <joe@example.com>'}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# This file was generated by the `rspec --init` command. Conventionally, all
|
||||
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
||||
# Require this file using `require "spec_helper"` to ensure that it is only
|
||||
# loaded once.
|
||||
#
|
||||
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
||||
|
||||
require 'i18n_extraction'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.treat_symbols_as_metadata_keys_with_true_values = true
|
||||
config.run_all_when_everything_filtered = true
|
||||
config.filter_run :focus
|
||||
|
||||
# Run specs in random order to surface order dependencies. If you find an
|
||||
# order dependency and want to debug it, you can fix the order by providing
|
||||
# the seed, which is printed after each run.
|
||||
# --seed 1234
|
||||
config.order = 'random'
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/bash
|
||||
result=0
|
||||
|
||||
echo "################ Running tests against Rails 2 ################"
|
||||
export CANVAS_RAILS3=0
|
||||
bundle install
|
||||
bundle exec rspec spec
|
||||
let result=$result+$?
|
||||
|
||||
echo "################ Running tests against Rails 3 ################"
|
||||
mv Gemfile.lock Gemfile.lock.rails2
|
||||
export CANVAS_RAILS3=true
|
||||
bundle install
|
||||
bundle exec rspec spec
|
||||
let result=$result+$?
|
||||
mv Gemfile.lock.rails2 Gemfile.lock
|
||||
|
||||
|
||||
if [ $result -eq 0 ]; then
|
||||
echo "SUCCESS"
|
||||
else
|
||||
echo "FAILURE"
|
||||
fi
|
||||
|
||||
exit $result
|
|
@ -1,5 +1,5 @@
|
|||
require 'fileutils'
|
||||
require 'lib/i18n_extraction/handlebars_extractor'
|
||||
require 'i18n_extraction'
|
||||
|
||||
# Precompiles handlebars templates into JavaScript function strings
|
||||
class EmberHbs
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require 'fileutils'
|
||||
require 'lib/i18n_extraction/handlebars_extractor'
|
||||
require 'compass'
|
||||
require 'sass/plugin'
|
||||
require 'i18n_extraction'
|
||||
|
||||
# Precompiles handlebars templates into JavaScript function strings
|
||||
class Handlebars
|
||||
|
|
|
@ -162,7 +162,7 @@ namespace :i18n do
|
|||
|
||||
require 'i18n'
|
||||
require 'i18nema'
|
||||
require 'lib/i18n_extraction/js_extractor.rb'
|
||||
require 'i18n_extraction'
|
||||
require 'lib/i18n/utils'
|
||||
|
||||
I18n.backend = I18nema::Backend.new
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
#
|
||||
# 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 File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
|
||||
|
||||
describe I18nExtraction::HandlebarsExtractor do
|
||||
def extract(source, scope = 'asdf', options = {})
|
||||
scope_results = scope && (options.has_key?(:scope_results) ? options.delete(:scope_results) : true)
|
||||
|
||||
extractor = I18nExtraction::HandlebarsExtractor.new
|
||||
extractor.process(source, scope)
|
||||
(scope_results ?
|
||||
scope.split(/\./).inject(extractor.translations) { |hash, s| hash[s] } :
|
||||
extractor.translations) || {}
|
||||
end
|
||||
|
||||
context "keys" do
|
||||
it "should allow valid string keys" do
|
||||
extract('{{#t "foo"}}Foo{{/t}}').should eql({'foo' => "Foo"})
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda{ extract '{{#t "foo foo"}}Foo{{/t}}' }.should raise_error 'invalid translation key "foo foo" on line 1'
|
||||
end
|
||||
end
|
||||
|
||||
context "well-formed-ness" do
|
||||
it "should make sure all #t calls are closed" do
|
||||
lambda{ extract "{{#t \"foo\"}}Foo{{/t}}\n{{#t \"bar\"}}...\nruh-roh\n" }.should raise_error /possibly unterminated #t call \(line 2/
|
||||
end
|
||||
end
|
||||
|
||||
context "values" do
|
||||
it "should strip extraneous whitespace" do
|
||||
extract("{{#t \"foo\"}}\t Foo\n foo\r\n\ffoo!!! {{/t}}").should eql({'foo' => 'Foo foo foo!!!'})
|
||||
end
|
||||
end
|
||||
|
||||
context "placeholders" do
|
||||
it "should allow simple placeholders" do
|
||||
extract('{{#t "foo"}}Hello {{user.name}}{{/t}}').should eql({'foo' => 'Hello %{user.name}'})
|
||||
end
|
||||
|
||||
it "should disallow helpers or anything else" do
|
||||
lambda{ extract '{{#t "foo"}}Hello {{call a helper}}{{/t}}' }.should raise_error 'helpers may not be used inside #t calls (line 1)'
|
||||
end
|
||||
end
|
||||
|
||||
context "wrappers" do
|
||||
it "should infer wrappers" do
|
||||
extract('{{#t "foo"}}Be sure to <a href="{{url}}">log in</a>. <b>Don\'t</b> you <b>dare</b> forget!!!{{/t}}').should eql({'foo' => 'Be sure to *log in*. **Don\'t** you **dare** forget!!!'})
|
||||
end
|
||||
|
||||
it "should not infer wrappers from unbalanced tags" do
|
||||
lambda{ extract '{{#t "foo"}}you are <b><i>so cool</i></strong>{{/t}}' }.should raise_error 'translation contains un-wrapper-ed markup (line 1). hint: use a placeholder, or balance your markup'
|
||||
end
|
||||
|
||||
it "should allow empty tags on either side of the wrapper" do
|
||||
extract('{{#t "bar"}}you can <button><i class="icon-email"></i>send an email</button>{{/t}}').should eql({'bar' => 'you can *send an email*'})
|
||||
extract('{{#t "baz"}}this is <b>so cool!<img /></b>{{/t}}').should eql({'baz' => 'this is *so cool!*'})
|
||||
end
|
||||
|
||||
it "should disallow any un-wrapper-ed html" do
|
||||
lambda{ extract '{{#t "foo"}}check out this pic: <img src="pic.gif">{{/t}}' }.should raise_error 'translation contains un-wrapper-ed markup (line 1). hint: use a placeholder, or balance your markup'
|
||||
end
|
||||
end
|
||||
|
||||
context "scoping" do
|
||||
it "should auto-scope relative keys to the current scope" do
|
||||
extract('{{#t "foo"}}Foo{{/t}}', 'asdf', :scope_results => false).should eql({'asdf' => {'foo' => "Foo"}})
|
||||
end
|
||||
|
||||
it "should not auto-scope absolute keys" do
|
||||
extract('{{#t "#foo"}}Foo{{/t}}', 'asdf', :scope_results => false).should eql({'foo' => "Foo"})
|
||||
end
|
||||
end
|
||||
|
||||
context "collisions" do
|
||||
it "should not let you reuse a key" do
|
||||
lambda{ extract '{{#t "foo"}}Foo{{/t}}{{#t "foo"}}foo{{/t}}' }.should raise_error 'cannot reuse key "asdf.foo"'
|
||||
end
|
||||
|
||||
it "should not let you use a scope as a key" do
|
||||
lambda{ extract '{{#t "foo.bar"}}bar{{/t}}{{#t "foo"}}foo{{/t}}' }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
|
||||
it "should not let you use a key as a scope" do
|
||||
lambda{ extract '{{#t "foo"}}foo{{/t}}{{#t "foo.bar"}}bar{{/t}}' }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,205 +0,0 @@
|
|||
#
|
||||
# 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 File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
|
||||
|
||||
describe I18nExtraction::JsExtractor do
|
||||
def extract(source, scope = 'asdf', options = {})
|
||||
scope_results = scope && (options.has_key?(:scope_results) ? options.delete(:scope_results) : true)
|
||||
|
||||
extractor = I18nExtraction::JsExtractor.new
|
||||
source = "require(['i18n!#{scope}'], function(I18n) {\n#{source.gsub(/^/, ' ')}\n});" if scope
|
||||
extractor.process(source, options)
|
||||
(scope_results ?
|
||||
scope.split(/\./).inject(extractor.translations) { |hash, s| hash[s] } :
|
||||
extractor.translations) || {}
|
||||
end
|
||||
|
||||
context "keys" do
|
||||
it "should allow valid string keys" do
|
||||
extract("I18n.t('foo', \"Foo\")").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda{ extract "I18n.t(foo, \"Foo\")" }.should raise_error /invalid key/
|
||||
lambda{ extract "I18n.t('f' + 'o' + 'o', \"Foo\"" }.should raise_error /unable to "parse" I18n call/
|
||||
lambda{ extract "I18n.t('f o o', \"Foo\"" }.should raise_error /unable to "parse" I18n call/
|
||||
end
|
||||
end
|
||||
|
||||
context "default translations" do
|
||||
it "should allow strings" do
|
||||
extract("I18n.t('foo', \"Foo\")").should == {'foo' => "Foo"}
|
||||
extract("I18n.t('foo', 'F' +\n'o' +\n'o')").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda{ extract "I18n.t('foo', foo)" }.should raise_error /unable to "parse" I18n call/
|
||||
lambda{ extract "I18n.t('foo', I18n.t('bar', 'bar'))" }.should raise_error /unable to "parse" I18n call/
|
||||
end
|
||||
|
||||
it "should complain if there is no default" do
|
||||
lambda{ extract("I18n.t('foo')")}.should raise_error(/no default provided for "asdf.foo"/)
|
||||
end
|
||||
|
||||
it "should skip t for core translations with no defaults" do
|
||||
extract("I18n.t('#date.month_names')", 'asdf', :scope_results => false).should == {}
|
||||
end
|
||||
end
|
||||
|
||||
context "placeholders" do
|
||||
it "should ensure all placeholders have corresponding options" do
|
||||
lambda{ extract "I18n.t('foo', 'i have a %{foo}')" }.should raise_error(/interpolation value not provided for :foo/)
|
||||
end
|
||||
end
|
||||
|
||||
context "pluralization" do
|
||||
it "should auto-pluralize single words + :count" do
|
||||
extract("I18n.t('foo', 'Foo', {count: 1})").should == {'foo' => {'one' => "1 Foo", 'other' => "%{count} Foos"}}
|
||||
end
|
||||
|
||||
it "should not auto-pluralize other strings + :count" do
|
||||
extract("I18n.t('foo', 'Foo foo', {count: 1})").should == {'foo' => "Foo foo"}
|
||||
end
|
||||
|
||||
it "should allow valid pluralization sub-keys" do
|
||||
extract("I18n.t('foo', {one: 'a foo', other: 'some foos'}, {count: 1})").should == {'foo' => {'one' => 'a foo', 'other' => 'some foos'}}
|
||||
end
|
||||
|
||||
it "should complain if not all required pluralization sub-keys are provided" do
|
||||
lambda{ extract("I18n.t('foo', {other: 'some foos'}, {count: 1})") }.should raise_error(/not all required :count sub-key\(s\) provided/)
|
||||
end
|
||||
|
||||
it "should reject invalid pluralization sub-keys" do
|
||||
lambda{ extract "I18n.t('foo', {invalid: '%{count} Foo'}, {count: 1})" }.should raise_error(/invalid :count sub-key\(s\):/)
|
||||
end
|
||||
end
|
||||
|
||||
context "nesting" do
|
||||
it "should correctly evaluate i18n calls that are arguments to other (possibly) i18n calls" do
|
||||
extract("I18n.t('foo', 'Foo said \"%{bar}\"', {bar: I18n.t('bar', 'Hello bar')})").should == {'foo' => 'Foo said "%{bar}"', 'bar' => 'Hello bar'}
|
||||
end
|
||||
end
|
||||
|
||||
context "scoping" do
|
||||
it "should correctly infer the scope" do
|
||||
extract(<<-SOURCE, nil).should == {'asdf' => {'bar' => 'Bar'}}
|
||||
define('bar',
|
||||
['foo',
|
||||
'i18n!asdf'
|
||||
], function(Foo, I18n) {
|
||||
I18n.t('bar', 'Bar');
|
||||
});
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should require a scope for all I18n calls" do
|
||||
lambda{ extract(<<-SOURCE, nil) }.should raise_error /possibly unscoped I18n call on line 2/
|
||||
require(['i18n'], function(I18n) {
|
||||
I18n.t('bar', 'Bar');
|
||||
});
|
||||
SOURCE
|
||||
|
||||
lambda{ extract(<<-SOURCE, nil) }.should raise_error /possibly unscoped I18n call on line 1/
|
||||
I18n.t('bar', 'Bar');
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should auto-scope relative keys to the current scope" do
|
||||
extract("I18n.t('foo', 'Foo')", 'asdf', :scope_results => false).should == {'asdf' => {'foo' => "Foo"}}
|
||||
end
|
||||
|
||||
it "should not auto-scope absolute keys" do
|
||||
extract("I18n.t('#foo', 'Foo')", 'asdf', :scope_results => false).should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should not allow multiple scopes" do
|
||||
lambda{ extract(<<-SOURCE, nil) }.should raise_error /multiple scopes are not allowed/
|
||||
require(['i18n!asdf'], function(I18n) {
|
||||
I18n.t('bar', 'Bar');
|
||||
});
|
||||
require(['i18n!qwerty'], function(I18n) {
|
||||
I18n.t('lol', 'Wut');
|
||||
});
|
||||
SOURCE
|
||||
end
|
||||
end
|
||||
|
||||
context "collisions" do
|
||||
it "should not let you reuse a key" do
|
||||
lambda{ extract "I18n.t('foo', 'Foo')\nI18n.t('foo', 'foo')" }.should raise_error 'cannot reuse key "asdf.foo"'
|
||||
end
|
||||
|
||||
it "should not let you use a scope as a key" do
|
||||
lambda{ extract "I18n.t('foo.bar', 'bar')\nI18n.t('foo', 'foo')" }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
|
||||
it "should not let you use a key as a scope" do
|
||||
lambda{ extract "I18n.t('foo', 'foo')\nI18n.t('foo.bar', 'bar')" }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
end
|
||||
|
||||
context "erb" do
|
||||
it "should support jt and I18n.l calls" do
|
||||
extract(<<-SOURCE, nil, :erb => true).should eql({}) # doesn't actually extract it
|
||||
<% js_block do %>
|
||||
<script>
|
||||
require(['i18n'], function(I18n) {
|
||||
I18n.l('asdf', 'asdf')
|
||||
<%= jt('bar', 'Bar') %>
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should not support the i18n AMD plugin" do
|
||||
lambda{ extract(<<-SOURCE, nil, :erb => true) }.should raise_error "i18n amd plugin is not supported in js_blocks (line 3)"
|
||||
<% js_block do %>
|
||||
<script>
|
||||
require(['i18n!scope'], function(I18n) {
|
||||
<%= jt('bar', 'Bar') %>
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should not allow jt calls outside of a require/define block" do
|
||||
lambda{ extract(<<-SOURCE, nil, :erb => true) }.should raise_error /possibly unscoped jt call on line 3/
|
||||
<% js_block do %>
|
||||
<script>
|
||||
<%= jt('bar', 'Bar') %>
|
||||
</script>
|
||||
<% end %>
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it "should not allow raw I18n calls" do
|
||||
lambda{ extract(<<-SOURCE, nil, :erb => true) }.should raise_error /raw I18n call on line 4/
|
||||
<% js_block do %>
|
||||
<script>
|
||||
require(['i18n'], function(I18n) {
|
||||
I18n.t('bar', 'Bar');
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
||||
SOURCE
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,208 +0,0 @@
|
|||
#
|
||||
# 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 File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
|
||||
|
||||
describe I18nExtraction::RubyExtractor do
|
||||
def extract(source, scope = 'asdf.', scope_results = true, in_html_view = false)
|
||||
sexps = RubyParser.new.parse(source)
|
||||
extractor = I18nExtraction::RubyExtractor.new
|
||||
extractor.scope = scope
|
||||
extractor.in_html_view = in_html_view
|
||||
extractor.process(sexps)
|
||||
(scope_results ?
|
||||
scope.split(/\./).inject(extractor.translations) { |hash, s| hash[s] } :
|
||||
extractor.translations) || {}
|
||||
end
|
||||
|
||||
context "well-formedness" do
|
||||
it "should complain about missing arguments" do
|
||||
lambda{ extract("t 'foo'")}.should raise_error /insufficient arguments for translate call/
|
||||
end
|
||||
end
|
||||
|
||||
context "keys" do
|
||||
it "should allow string keys" do
|
||||
extract("t 'foo', \"Foo\"").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should allow symbol keys" do
|
||||
extract("t :foo, \"Foo\"").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda{ extract "t foo, \"Foo\"" }.should raise_error /invalid translation key/
|
||||
lambda{ extract "t \"f\#{o}o\", \"Foo\"" }.should raise_error /invalid translation key/
|
||||
lambda{ extract "t true ? :foo : :bar, \"Foo\"" }.should raise_error /invalid translation key/
|
||||
end
|
||||
end
|
||||
|
||||
context "default translations" do
|
||||
it "should allow strings" do
|
||||
extract("t 'foo', \"Foo\"").should == {'foo' => "Foo"}
|
||||
extract("t 'foo2', <<-STR\nFoo\nSTR").should == {'foo2' => "Foo"}
|
||||
extract("t 'foo', 'F' + 'o' + 'o'").should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should disallow everything else" do
|
||||
lambda{ extract "t 'foo', \"F\#{o}o\"" }.should raise_error /invalid en default/
|
||||
lambda{ extract "t 'foo', foo" }.should raise_error /invalid en default/
|
||||
lambda{ extract "t 'foo', (true ? 'Foo' : 'Bar')" }.should raise_error /invalid en default/
|
||||
end
|
||||
end
|
||||
|
||||
context "placeholders" do
|
||||
it "should ensure all placeholders have corresponding options" do
|
||||
lambda{ extract "t 'foo', 'i have a %{foo}'" }.should raise_error(/interpolation value not provided for :foo/)
|
||||
extract("t 'foo', 'i have a %{foo}', :foo => 'foo'").should == {'foo' => 'i have a %{foo}'}
|
||||
lambda{ extract "jt 'foo', 'i have a %{foo}', '{}'" }.should raise_error(/interpolation value not provided for :foo/)
|
||||
extract("jt 'foo', 'i have a %{foo}', '{foo: a_foo}'").should == {'foo' => 'i have a %{foo}'}
|
||||
end
|
||||
end
|
||||
|
||||
context "pluralization" do
|
||||
it "should auto-pluralize single words + :count" do
|
||||
extract("t 'foo', 'Foo', :count => 1").should == {'foo' => {'one' => "1 Foo", 'other' => "%{count} Foos"}}
|
||||
end
|
||||
|
||||
it "should not auto-pluralize other strings + :count" do
|
||||
extract("t 'foo', 'Foo foo', :count => 1").should == {'foo' => "Foo foo"}
|
||||
end
|
||||
|
||||
it "should allow valid pluralization sub-keys" do
|
||||
extract("t 'foo', {:one => 'a foo', :other => 'some foos'}, :count => 1").should == {'foo' => {'one' => 'a foo', 'other' => 'some foos'}}
|
||||
end
|
||||
|
||||
it "should complain if not all required pluralization sub-keys are provided" do
|
||||
lambda{ extract("t 'foo', {:other => 'some foos'}, :count => 1") }.should raise_error(/not all required :count sub-key\(s\) provided/)
|
||||
end
|
||||
|
||||
it "should reject invalid pluralization sub-keys" do
|
||||
lambda{ extract "t 'foo', {:invalid => '%{count} Foo'}, :count => 1" }.should raise_error(/invalid :count sub-key\(s\)/)
|
||||
end
|
||||
end
|
||||
|
||||
context "labels" do
|
||||
it "should interpret symbol names as the key" do
|
||||
extract("label :thing, :the_foo, :foo, :en => 'Foo'").should == {'labels' => {"foo" => "Foo"}}
|
||||
extract("f.label :the_foo, :foo, :en => 'Foo'").should == {'labels' => {"foo" => "Foo"}}
|
||||
extract("label_tag :the_foo, :foo, :en => 'Foo'").should == {'labels' => {"foo" => "Foo"}}
|
||||
end
|
||||
|
||||
it "should infer the key from the method if not provided" do
|
||||
extract("label :thing, :the_foo, :en => 'Foo'").should == {'labels' => {"the_foo" => "Foo"}}
|
||||
extract("f.label :the_foo, :en => 'Foo'").should == {'labels' => {"the_foo" => "Foo"}}
|
||||
extract("label_tag :the_foo, :en => 'Foo'").should == {'labels' => {"the_foo" => "Foo"}}
|
||||
end
|
||||
|
||||
it "should skip label calls with non-symbol keys (i.e. just a standard label)" do
|
||||
extract("label :thing, :the_foo, 'foo'").should == {}
|
||||
extract("f.label :the_foo, 'foo'").should == {}
|
||||
extract("label_tag :thing, 'foo'").should == {}
|
||||
end
|
||||
|
||||
it "should complain if a label call has a non-symbol key and a default" do
|
||||
lambda{ extract "label :thing, :the_foo, 'foo', :en => 'Foo'" }.should raise_error /invalid translation key/
|
||||
lambda{ extract "f.label :the_foo, 'foo', :en => 'Foo'" }.should raise_error /invalid translation key/
|
||||
lambda{ extract "label_tag :the_foo, 'foo', :en => 'Foo'" }.should raise_error /invalid translation key/
|
||||
end
|
||||
|
||||
it "should not auto-scope absolute keys" do
|
||||
extract("label :thing, :the_foo, :'#foo', :en => 'Foo'", '').should == {"foo" => "Foo"}
|
||||
extract("f.label :the_foo, :'#foo', :en => 'Foo'", '').should == {"foo" => "Foo"}
|
||||
extract("label_tag :the_foo, :'#foo', :en => 'Foo'", '').should == {"foo" => "Foo"}
|
||||
end
|
||||
|
||||
it "should complain if no default is provided" do
|
||||
lambda{ extract "label :thing, :the_foo, :foo" }.should raise_error /invalid\/missing en default nil/
|
||||
lambda{ extract "f.label :the_foo, :foo" }.should raise_error /invalid\/missing en default nil/
|
||||
lambda{ extract "label_tag :the_foo, :foo" }.should raise_error /invalid\/missing en default nil/
|
||||
end
|
||||
end
|
||||
|
||||
context "nesting" do
|
||||
it "should correctly evaluate i18n calls that are arguments to other (possibly) i18n calls" do
|
||||
extract("t :foo, 'Foo said \"%{bar}\"', :bar => t(:bar, 'Hello bar')").should == {'foo' => 'Foo said "%{bar}"', 'bar' => 'Hello bar'}
|
||||
extract("label :thing, :the_foo, t(:bar, 'Bar')").should == {'bar' => 'Bar'}
|
||||
extract("f.label :the_foo, t(:bar, 'Bar')").should == {'bar' => 'Bar'}
|
||||
end
|
||||
|
||||
it "should ignore i18n calls within i18n method definitions" do
|
||||
extract("def t(*args); other.t *args; end").should == {}
|
||||
end
|
||||
end
|
||||
|
||||
context "scoping" do
|
||||
it "should auto-scope relative keys to the current scope" do
|
||||
extract("t 'foo', 'Foo'", 'asdf.', false).should == {'asdf' => {'foo' => "Foo"}}
|
||||
end
|
||||
|
||||
it "should not auto-scope absolute keys" do
|
||||
extract("t '#foo', 'Foo'", 'asdf.', false).should == {'foo' => "Foo"}
|
||||
end
|
||||
|
||||
it "should not auto-scope keys with I18n as the receiver" do
|
||||
extract("I18n.t 'foo', 'Foo'", 'asdf.', false).should == {'foo' => 'Foo'}
|
||||
end
|
||||
|
||||
it "should auto-scope plugin registration" do
|
||||
extract("Canvas::Plugin.register('dim_dim', :web_conferencing, {:name => lambda{ t :name, \"DimDim\" }})", '', false).should ==
|
||||
{'plugins' => {'dim_dim' => {'name' => "DimDim"}}}
|
||||
end
|
||||
|
||||
it "should require explicit keys if there is no scope" do
|
||||
lambda{ extract("t 'foo', 'Foo'", '') }.should raise_error /ambiguous translation key "foo"/
|
||||
end
|
||||
end
|
||||
|
||||
context "collisions" do
|
||||
it "should not let you reuse a key" do
|
||||
lambda{ extract "t 'foo', 'Foo'\nt 'foo', 'foo'" }.should raise_error 'cannot reuse key "asdf.foo"'
|
||||
end
|
||||
|
||||
it "should not let you use a scope as a key" do
|
||||
lambda{ extract "t 'foo.bar', 'bar'\nt 'foo', 'foo'" }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
|
||||
it "should not let you use a key as a scope" do
|
||||
lambda{ extract "t 'foo', 'foo'\nt 'foo.bar', 'bar'" }.should raise_error '"asdf.foo" used as both a scope and a key'
|
||||
end
|
||||
end
|
||||
|
||||
context "whitespace" do
|
||||
it "should remove extraneous whitespace from view translate calls" do
|
||||
extract("t 'foo', \"\\n Foo \\t foo!\\n\"", 'asdf.', true, true).should == {'foo' => "Foo foo!"}
|
||||
end
|
||||
|
||||
it "should strip whitespace from all other calls" do
|
||||
extract("t 'foo', \"\\n Foo \\t foo!\\n\"").should == {'foo' => "Foo \t foo!"}
|
||||
extract("mt 'foo', \"\\n Foo \\t foo!\\n\"").should == {'foo' => "Foo \t foo!"}
|
||||
end
|
||||
end
|
||||
|
||||
context "sanitization" do
|
||||
it "should reject stuff that looks sufficiently html-y" do
|
||||
lambda{ extract "t 'dude', 'this is <em>important</em>'" }.should raise_error /html tags on line 1/
|
||||
end
|
||||
|
||||
it "should generally be ok with angle brackets" do
|
||||
extract("t 'obvious', 'TIL 1 < 2'").should == {'obvious' => 'TIL 1 < 2'}
|
||||
extract("t 'email', 'please enter an email, e.g. Joe User <joe@example.com>'").should == {'email' => 'please enter an email, e.g. Joe User <joe@example.com>'}
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue