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:
Mark Severson 2014-03-05 16:54:13 -07:00 committed by Simon Williams
parent b6cbbdc0cd
commit 4fbe46509c
23 changed files with 676 additions and 576 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
--color
--format progress

View File

@ -0,0 +1,3 @@
source 'https://rubygems.org'
gemspec

View File

@ -0,0 +1 @@
require "bundler/gem_tasks"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

25
gems/i18n_extraction/test.sh Executable file
View File

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

View File

@ -1,5 +1,5 @@
require 'fileutils'
require 'lib/i18n_extraction/handlebars_extractor'
require 'i18n_extraction'
# Precompiles handlebars templates into JavaScript function strings
class EmberHbs

View File

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

View File

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

View File

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

View File

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

View File

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