i18nliner 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,111 @@
1
+ require 'i18nliner/call_helpers'
2
+ require 'i18nliner/errors'
3
+
4
+ module I18nliner
5
+ module Extractors
6
+ class TranslateCall
7
+ include CallHelpers
8
+
9
+ def initialize(scope, line, receiver, method, args)
10
+ @scope = scope
11
+ @line = line
12
+ @receiver = receiver
13
+ @method = method
14
+
15
+ normalize_arguments(args)
16
+
17
+ validate
18
+ normalize
19
+ end
20
+
21
+ def validate
22
+ validate_key
23
+ validate_default
24
+ validate_options
25
+ end
26
+
27
+ def normalize
28
+ @key = normalize_key(@key, @scope, @receiver)
29
+ @default = normalize_default(@default, @options || {})
30
+ end
31
+
32
+ def translations
33
+ return [] unless @default
34
+ return [[@key, @default]] if @default.is_a?(String)
35
+ @default.map { |key, value|
36
+ ["#{@key}.#{key}", value]
37
+ }
38
+ end
39
+
40
+ def validate_key
41
+ end
42
+
43
+ def validate_default
44
+ return unless @default.is_a?(Hash)
45
+ if (keys = @default.keys - ALLOWED_PLURALIZATION_KEYS).size > 0
46
+ raise InvalidPluralizationKeyError.new(@line, keys)
47
+ elsif REQUIRED_PLURALIZATION_KEYS & (keys = @default.keys) != REQUIRED_PLURALIZATION_KEYS
48
+ raise MissingPluralizationKeyError.new(@line, keys)
49
+ else
50
+ @default.values.each do |value|
51
+ raise InvalidPluralizationDefaultError.new(@line, value) unless value.is_a?(String)
52
+ end
53
+ end
54
+
55
+ unless I18nliner.infer_interpolation_values
56
+ if @default.is_a?(String)
57
+ validate_interpolation_values(@key, @default)
58
+ else
59
+ @default.each_pair do |sub_key, default|
60
+ validate_interpolation_values("#{@key}.#{sub_key}", default)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ # Possible translate signatures:
69
+ #
70
+ # key [, options]
71
+ # key, default_string [, options]
72
+ # key, default_hash, options
73
+ # default_string [, options]
74
+ # default_hash, options
75
+ def normalize_arguments(args)
76
+ raise InvalidSignatureError.new(@line, args) if args.empty?
77
+
78
+ has_key = key_provided?(@scope, @receiver, *args)
79
+ args.unshift infer_key(args[0]) if !has_key && args[0].is_a?(String) || args[0].is_a?(Hash)
80
+
81
+ # [key, options] -> [key, nil, options]
82
+ args.insert(1, nil) if has_key && args[1].is_a?(Hash) && args[2].nil?
83
+
84
+ @key, @default, @options, *others = args
85
+
86
+ raise InvalidSignatureError.new(@line, args) if !others.empty?
87
+ raise InvalidSignatureError.new(@line, args) unless @key.is_a?(Symbol) || @key.is_a?(String)
88
+ raise InvalidSignatureError.new(@line, args) unless @default.nil? || @default.is_a?(String) || @default.is_a?(Hash)
89
+ raise InvalidSignatureError.new(@line, args) unless @options.nil? || @options.is_a?(Hash)
90
+ end
91
+
92
+ def validate_interpolation_values(key, default)
93
+ default.scan(/%\{([^\}]+)\}/) do |match|
94
+ placeholder = match[0].to_sym
95
+ next if @options.include?(placeholder)
96
+ raise MissingInterpolationValueError.new(@line, placeholder)
97
+ end
98
+ end
99
+
100
+ def validate_options
101
+ if @default.is_a?(Hash)
102
+ raise MissingCountValueError.new(@line) unless @options && @options.key?(:count)
103
+ end
104
+ return if @options.nil?
105
+ @options.keys.each do |key|
106
+ raise InvalidOptionKeyError.new(@line) unless key.is_a?(String) || key.is_a?(Symbol)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,45 @@
1
+ module I18nLine
2
+ module Extractors
3
+ class TranslationHash < Hash
4
+ attr_accessor :line
5
+
6
+ def self.new(hash)
7
+ hash.is_a?(self) ? hash : super
8
+ end
9
+
10
+ def initialize(*args)
11
+ super
12
+ @total_size = 0
13
+ end
14
+
15
+ def []=(key, value)
16
+ parts = key.split('.')
17
+ leaf = parts.pop
18
+ hash = self
19
+ while part = parts.shift
20
+ if hash[part]
21
+ unless hash[part].is_a?(Hash)
22
+ intermediate_key = key.sub((parts + [leaf]).join('.'), '')
23
+ raise KeyAsScopeError, intermediate_key
24
+ end
25
+ else
26
+ hash[part] = {}
27
+ end
28
+ hash = hash[part]
29
+ end
30
+ if hash[leaf]
31
+ if hash[leaf] != default
32
+ if hash[leaf].is_a?(Hash)
33
+ raise KeyAsScopeError.new(@line, key)
34
+ else
35
+ raise KeyInUseError.new(@line, key)
36
+ end
37
+ end
38
+ else
39
+ @total_size += 1
40
+ hash[key] = value
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ module I18nliner
2
+ module Processors
3
+ class AbstractProcessor
4
+ def initialize(translations, options = {})
5
+ @translations = translations
6
+ @only = options[:only]
7
+ @checker = options[:checker] || methods(:noop_checker)
8
+ end
9
+
10
+ def noop_checker(file)
11
+ yield file
12
+ end
13
+
14
+ def files
15
+ @files ||= begin
16
+ files = Globby.select(@pattern)
17
+ files = files.select(@only) if @only
18
+ files.reject(I18nliner.ignore)
19
+ end
20
+ end
21
+
22
+ def check_files
23
+ files.each do |file|
24
+ @checker.call file, &methods(:check_file)
25
+ end
26
+ end
27
+
28
+ def self.inherited(klass)
29
+ Processors.register klass
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ module I18nliner
2
+ module Processors
3
+ class ErbProcessor < RubyProcessor
4
+ def source_for(file)
5
+ # TODO: pre-process for block fu
6
+ Erubis::Eruby.new(super).src
7
+ end
8
+
9
+ def scope_for(path)
10
+ scope = path.gsub(/(\A|.*\/)app\/views\/|\.html\z|(\.html)?\.erb\z/, '')
11
+ scope = scope.gsub(/\/_?/, '.')
12
+ Scope.new(scope, :allow_relative => true)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module I18nliner
2
+ module Processors
3
+ class RubyProcessor < AbstractProcessor
4
+ def check_file(file)
5
+ sexps = RubyParser.new.parse(source_for(file))
6
+ extractor = Extractors::RubyExtractor.new(sexps, scope_for(file))
7
+ extractor.each_translation do |key, value|
8
+ @translations.line = extractor.line
9
+ @translations[key] = value
10
+ end
11
+ end
12
+
13
+ def source_for(file)
14
+ File.read(file)
15
+ end
16
+
17
+ def scope_for(path)
18
+ Scope.new
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module I18nliner
2
+ module Processors
3
+ def self.register(klass)
4
+ (@processors ||= []) << klass
5
+ end
6
+
7
+ def self.all
8
+ @processors.dup
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ module I18nliner
2
+ class Scope
3
+ attr_reader :scope
4
+
5
+ def initialize(scope = nil, options = {})
6
+ @scope = scope ? "#{scope}." : scope
7
+ @options = {
8
+ :allow_relative => false
9
+ }.merge(options)
10
+ end
11
+
12
+ def allow_relative?
13
+ @options[:allow_relative]
14
+ end
15
+
16
+ def normalize_key(key)
17
+ if allow_relative? && (key = key.dup) && key.sub!(/\A\./, '')
18
+ scope + key
19
+ else
20
+ key
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/i18nliner.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ module I18nliner
4
+ def self.translations
5
+ end
6
+
7
+ def self.look_up(*args)
8
+ end
9
+
10
+ def self.setting(key, value)
11
+ instance_eval <<-CODE
12
+ def #{key}(value = nil)
13
+ if value && block_given?
14
+ begin
15
+ value_was = @#{key}
16
+ @#{key} = value
17
+ yield
18
+ ensure
19
+ @#{key} = value_was
20
+ end
21
+ else
22
+ @#{key} = #{value.inspect} if @#{key}.nil?
23
+ @#{key}
24
+ end
25
+ end
26
+ CODE
27
+ end
28
+
29
+ setting :inferred_key_format, :underscored_crc32
30
+ setting :infer_interpolation_values, true
31
+ end
@@ -0,0 +1,14 @@
1
+ namespace :i18nliner do
2
+ desc "Verifies all translation calls"
3
+ task :check => :environment do
4
+ options = {:only => ENV['ONLY'])}
5
+ @command = I18nliner::Commands::Check.run(options) or exit 1
6
+ end
7
+
8
+ desc "Generates a new [default_locale].yml file for all translations"
9
+ task :dump => :check do
10
+ options = {:translations => @command.translations}
11
+ @command = I18nliner::Commands::Dump.run(options) or exit 1
12
+ end
13
+ end
14
+
@@ -0,0 +1,61 @@
1
+ require 'sexp_processor'
2
+ require 'ruby_parser'
3
+ require 'i18nliner'
4
+ require 'i18nliner/errors'
5
+ require 'i18nliner/scope'
6
+ require 'i18nliner/extractors/ruby_extractor'
7
+ require 'i18nliner/extractors/translate_call'
8
+
9
+ describe I18nliner::Extractors::RubyExtractor do
10
+ def extract(source, scope = I18nliner::Scope.new(nil))
11
+ sexps = RubyParser.new.parse(source)
12
+ extractor = I18nliner::Extractors::RubyExtractor.new(sexps, scope)
13
+ translations = []
14
+ extractor.each_translation { |translation| translations << translation }
15
+ Hash[translations]
16
+ end
17
+
18
+ def assert_error(*args)
19
+ error = args.pop
20
+ expect {
21
+ extract(*args)
22
+ }.to raise_error(error)
23
+ end
24
+
25
+ describe "#each_translation" do
26
+ it "should ignore non-t calls" do
27
+ extract("foo 'Foo'").should == {}
28
+ end
29
+
30
+ it "should not extract t calls with no default" do
31
+ extract("t :foo").should == {}
32
+ end
33
+
34
+ it "should extract valid t calls" do
35
+ if false
36
+ extract("t 'Foo'").should ==
37
+ {"foo_f44ad75d" => "Foo"}
38
+ extract("t :bar, 'Baz'").should ==
39
+ {"bar" => "Baz"}
40
+ extract("t 'lol', 'wut'").should ==
41
+ {"lol" => "wut"}
42
+ extract("translate 'one', {:one => '1', :other => '2'}, :count => 1").should ==
43
+ {"one.one" => "1", "one.other" => "2"}
44
+ extract("t({:one => 'just one', :other => 'zomg lots'}, :count => 1)").should ==
45
+ {"zomg_lots_a54248c9.one" => "just one", "zomg_lots_a54248c9.other" => "zomg lots"}
46
+ extract("t 'foo2', <<-STR\nFoo\nSTR").should ==
47
+ {'foo2' => "Foo"}
48
+ end
49
+ extract("t 'foo', 'F' + 'o' + 'o'").should ==
50
+ {'foo' => "Foo"}
51
+ end
52
+
53
+ it "should bail on invalid t calls" do
54
+ assert_error "t foo", I18nliner::InvalidSignatureError
55
+ assert_error "t :foo, foo", I18nliner::InvalidSignatureError
56
+ assert_error "t :foo, \"hello \#{man}\"", I18nliner::InvalidSignatureError
57
+ assert_error "t :a, \"a\", {}, {}", I18nliner::InvalidSignatureError
58
+ assert_error "t({:one => '1', :other => '2'})", I18nliner::MissingCountValueError
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,154 @@
1
+ require 'i18nliner'
2
+ require 'i18nliner/scope'
3
+ require 'i18nliner/extractors/translate_call'
4
+
5
+ describe I18nliner::Extractors::TranslateCall do
6
+ def call(scope, *args)
7
+ I18nliner::Extractors::TranslateCall.new(scope, nil, nil, :t, args)
8
+ end
9
+
10
+ let(:no_scope) { I18nliner::Scope.new(nil) }
11
+ let(:scope) { I18nliner::Scope.new("foo", :auto => true, :allow_relative => true) }
12
+
13
+ describe "signature" do
14
+ it "should reject extra arguments" do
15
+ expect {
16
+ call(no_scope, :key, "value", {}, :wat)
17
+ }.to raise_error(I18nliner::InvalidSignatureError)
18
+ end
19
+
20
+ it "should accept a valid key or default" do
21
+ expect {
22
+ call(no_scope, "key", "value", {})
23
+ }.to_not raise_error
24
+
25
+ expect {
26
+ call(no_scope, "key_or_value", {})
27
+ }.to_not raise_error
28
+
29
+ expect {
30
+ call(no_scope, :key, {})
31
+ }.to_not raise_error
32
+ end
33
+
34
+ it "should require at least a key or default" do
35
+ expect {
36
+ call(no_scope)
37
+ }.to raise_error(I18nliner::InvalidSignatureError)
38
+ end
39
+
40
+ it "should require a literal default" do
41
+ expect {
42
+ call(no_scope, :key, Object.new)
43
+ }.to raise_error(I18nliner::InvalidSignatureError)
44
+ end
45
+
46
+ it "should ensure options is a hash, if provided" do
47
+ expect {
48
+ call(no_scope, :key, "value", Object.new)
49
+ }.to raise_error(I18nliner::InvalidSignatureError)
50
+ end
51
+ end
52
+
53
+ describe "key inference" do
54
+ it "should generate literal keys" do
55
+ I18nliner.inferred_key_format :literal do
56
+ call(no_scope, "zomg key").translations.should ==
57
+ [["zomg key", "zomg key"]]
58
+ end
59
+ end
60
+
61
+ it "should generate underscored keys" do
62
+ I18nliner.inferred_key_format :underscored do
63
+ call(no_scope, "zOmg key!!").translations.should ==
64
+ [["zomg_key", "zOmg key!!"]]
65
+ end
66
+ end
67
+
68
+ it "should generate underscored + crc32 keys" do
69
+ I18nliner.inferred_key_format :underscored_crc32 do
70
+ call(no_scope, "zOmg key!!").translations.should ==
71
+ [["zomg_key_90a85b0b", "zOmg key!!"]]
72
+ end
73
+ end
74
+ end
75
+
76
+ describe "normalization" do
77
+ it "should make keys absolute if scoped" do
78
+ call(scope, '.key', "value").translations[0][0].should =~ /\Afoo\.key/
79
+ end
80
+
81
+ it "should strip whitespace from defaults" do
82
+ call(no_scope, "\t whitespace \n\t ").translations[0][1].should == "whitespace"
83
+ end
84
+ end
85
+
86
+ describe "pluralization" do
87
+ describe "defaults" do
88
+ it "should be inferred" do
89
+ translations = call(no_scope, "person", {:count => Object.new}).translations
90
+ translations.map(&:last).sort.should == ["%{count} people", "1 person"]
91
+ end
92
+
93
+ it "should not be inferred if given multiple words" do
94
+ translations = call(no_scope, "happy person", {:count => Object.new}).translations
95
+ translations.map(&:last).should == ["happy person"]
96
+ end
97
+ end
98
+
99
+ it "should accept valid hashes" do
100
+ call(no_scope, {:one => "asdf", :other => "qwerty"}, :count => 1).translations.sort.should ==
101
+ [["qwerty_98185351.one", "asdf"], ["qwerty_98185351.other", "qwerty"]]
102
+ call(no_scope, :some_stuff, {:one => "asdf", :other => "qwerty"}, :count => 1).translations.sort.should ==
103
+ [["some_stuff.one", "asdf"], ["some_stuff.other", "qwerty"]]
104
+ end
105
+
106
+ it "should reject invalid keys" do
107
+ expect {
108
+ call(no_scope, {:one => "asdf", :twenty => "qwerty"}, :count => 1)
109
+ }.to raise_error(I18nliner::InvalidPluralizationKeyError)
110
+ end
111
+
112
+ it "should require essential keys" do
113
+ expect {
114
+ call(no_scope, {:one => "asdf"}, :count => 1)
115
+ }.to raise_error(I18nliner::MissingPluralizationKeyError)
116
+ end
117
+
118
+ it "should reject invalid count defaults" do
119
+ expect {
120
+ call(no_scope, {:one => "asdf", :other => Object.new}, :count => 1)
121
+ }.to raise_error(I18nliner::InvalidPluralizationDefaultError)
122
+ end
123
+
124
+ it "should complain if no :count is provided" do
125
+ expect {
126
+ call(no_scope, {:one => "asdf", :other => "qwerty"})
127
+ }.to raise_error(I18nliner::MissingCountValueError)
128
+ end
129
+ end
130
+
131
+ describe "validation" do
132
+ it "should require all interpolation values" do
133
+ I18nliner.infer_interpolation_values false do
134
+ expect {
135
+ call(no_scope, "asdf %{bob}")
136
+ }.to raise_error(I18nliner::MissingInterpolationValueError)
137
+ end
138
+ end
139
+
140
+ it "should require all interpolation values in count defaults" do
141
+ I18nliner.infer_interpolation_values false do
142
+ expect {
143
+ call(no_scope, {:one => "asdf %{bob}", :other => "querty"})
144
+ }.to raise_error(I18nliner::MissingInterpolationValueError)
145
+ end
146
+ end
147
+
148
+ it "should ensure option keys are symbols or strings" do
149
+ expect {
150
+ call(no_scope, "hello", {Object.new => "okay"})
151
+ }.to raise_error(I18nliner::InvalidOptionKeyError)
152
+ end
153
+ end
154
+ end