i18nliner 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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