balinterdi-missing_t 0.1.0

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.
data/README.rdoc ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('missing_t', '0.1.0') do |p|
6
+ p.description = "See all the missing I18n translations in your Rails project"
7
+ p.url = "http://github.com/balinterdi/missing_t"
8
+ p.author = "Balint Erdi"
9
+ p.email = "balint.erdi@gmail.com"
10
+ p.ignore_pattern = ["tmp/*", "script/*"]
11
+ p.development_dependencies = []
12
+ end
13
+
14
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
data/bin/missing_t ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
4
+
5
+ require 'missing_t'
6
+
7
+ MissingT.new.find_missing_translations(ARGV[0]).each do |file, queries|
8
+ puts
9
+ puts "#{file}:"
10
+ puts
11
+ queries.each { |q| puts " #{q}" }
12
+ end
13
+
14
+
data/lib/missing_t.rb ADDED
@@ -0,0 +1,171 @@
1
+ require "yaml"
2
+ require "forwardable"
3
+ require "pp"
4
+ require "ruby-debug"
5
+
6
+ class Hash
7
+ def has_nested_key?(key)
8
+ h = self
9
+ key.to_s.split('.').each do |segment|
10
+ return false unless h.key?(segment)
11
+ h = h[segment]
12
+ end
13
+ true
14
+ end
15
+
16
+ # idea snatched from deep_merge in Rails source code
17
+ def deep_safe_merge(other_hash)
18
+ self.merge(other_hash) do |key, oldval, newval|
19
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
20
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
21
+ if oldval.class.to_s == 'Hash'
22
+ if newval.class.to_s == 'Hash'
23
+ oldval.deep_safe_merge(newval)
24
+ else
25
+ oldval
26
+ end
27
+ else
28
+ newval
29
+ end
30
+ end
31
+ end
32
+
33
+ def deep_safe_merge!(other_hash)
34
+ replace(deep_safe_merge(other_hash))
35
+ end
36
+
37
+ end
38
+
39
+ class MissingT
40
+ extend Forwardable
41
+ def_delegators :@translations, :[]
42
+
43
+ # attr_reader :translations
44
+
45
+ def initialize
46
+ @translations = Hash.new
47
+ end
48
+
49
+ # NOTE: this method is needed
50
+ # because attr_reader :translations
51
+ # does not seem to be stubbable
52
+ def translations
53
+ @translations
54
+ end
55
+
56
+ def add_translations(trs)
57
+ translations.deep_safe_merge!(trs)
58
+ end
59
+
60
+ def collect_translations
61
+ locales_pathes = ["config/locales/**/*.yml", "vendor/plugins/**/config/locales/**/*yml", "vendor/plugins/**/locale/**/*yml"]
62
+ locales_pathes.each do |path|
63
+ Dir.glob(path) do |file|
64
+ add_translations(translations_in_file(file))
65
+ end
66
+ end
67
+ end
68
+
69
+ def hashify(strings)
70
+ h = Hash.new
71
+ strings.map { |s| s.split('.') }.
72
+ each do |segmented_string|
73
+ root = h
74
+ segmented_string.each do |segment|
75
+ root[segment] ||= {}
76
+ root = root[segment]
77
+ end
78
+ end
79
+ h
80
+ end
81
+
82
+ def translations_in_file(yaml_file)
83
+ open(yaml_file) { |f| YAML.load(f.read) }
84
+ end
85
+
86
+ def files_with_i18n_queries
87
+ [ Dir.glob("app/**/*.erb"),
88
+ Dir.glob("app/**/controllers/**/*.rb"),
89
+ Dir.glob("app/**/helpers/**/*.rb")].flatten
90
+ end
91
+
92
+ def get_content_of_file_with_i18n_queries(file)
93
+ f = open(File.expand_path(file), "r")
94
+ content = f.read()
95
+ f.close()
96
+ content
97
+ end
98
+
99
+ def extract_i18n_queries(file)
100
+ i18n_query_pattern = /I18n\.(?:translate|t)\s*\((.*?)[,\)]/
101
+ get_content_of_file_with_i18n_queries(file).
102
+ scan(i18n_query_pattern).map { |match| match.first.gsub(/[^\w\.]/, '') }
103
+ end
104
+
105
+ def collect_translation_queries
106
+ queries = {}
107
+ files_with_i18n_queries.each do |file|
108
+ queries_in_file = extract_i18n_queries(file)
109
+ unless queries_in_file.empty?
110
+ queries[file] = queries_in_file
111
+ end
112
+ end
113
+ queries
114
+ #TODO: remove duplicate queries across files
115
+ end
116
+
117
+ def has_translation?(lang, query)
118
+ t = translations
119
+ i18n_label(lang, query).split('.').each do |segment|
120
+ return false unless (t.respond_to?(:key?) and t.key?(segment))
121
+ t = t[segment]
122
+ end
123
+ true
124
+ end
125
+
126
+ def get_missing_translations(queries, lang=nil)
127
+ missing = {}
128
+ languages = lang.nil? ? translations.keys : [lang]
129
+ languages.each do |l|
130
+ get_missing_translations_for_lang(queries, l).each do |file, qs|
131
+ missing[file] ||= []
132
+ missing[file].concat(qs).uniq!
133
+ end
134
+ end
135
+ missing
136
+ end
137
+
138
+ def find_missing_translations(lang=nil)
139
+ collect_translations
140
+ get_missing_translations(collect_translation_queries, lang)
141
+ end
142
+
143
+ private
144
+ def get_missing_translations_for_lang(queries, lang)
145
+ queries.map do |file, queries_in_file|
146
+ queries_with_no_translation = queries_in_file.select { |q| !has_translation?(lang, q) }
147
+ if queries_with_no_translation.empty?
148
+ nil
149
+ else
150
+ [file, queries_with_no_translation.map { |q| i18n_label(lang, q) }]
151
+ end
152
+ end.compact
153
+
154
+ end
155
+
156
+ def i18n_label(lang, query)
157
+ "#{lang}.#{query}"
158
+ end
159
+
160
+ end
161
+
162
+ if __FILE__ == $0
163
+ # puts "ARGV[1] = #{ARGV[0]}"
164
+ # pp MissingT.new.find_missing_translations(ARGV[0]).values.inject(0) { |sum, qs| sum + qs.length }
165
+ MissingT.new.find_missing_translations(ARGV[0]).each do |file, queries|
166
+ puts
167
+ puts "#{file}:"
168
+ puts
169
+ queries.each { |q| puts " #{q}" }
170
+ end
171
+ end
data/missing_t.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{missing_t}
5
+ s.version = "0.1.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Balint Erdi"]
9
+ s.date = %q{2009-03-03}
10
+ s.default_executable = %q{missing_t}
11
+ s.description = %q{See all the missing I18n translations in your Rails project}
12
+ s.email = %q{balint.erdi@gmail.com}
13
+ s.executables = ["missing_t"]
14
+ s.extra_rdoc_files = ["bin/missing_t", "lib/missing_t.rb", "README.rdoc", "tasks/missing_t.rake"]
15
+ s.files = ["bin/missing_t", "lib/missing_t.rb", "Manifest", "Rakefile", "README.rdoc", "spec/missing_t_spec.rb", "spec/spec_helper.rb", "tasks/missing_t.rake", "missing_t.gemspec"]
16
+ s.has_rdoc = true
17
+ s.homepage = %q{http://github.com/balinterdi/missing_t}
18
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Missing_t", "--main", "README.rdoc"]
19
+ s.require_paths = ["lib"]
20
+ s.rubyforge_project = %q{missing_t}
21
+ s.rubygems_version = %q{1.3.1}
22
+ s.summary = %q{See all the missing I18n translations in your Rails project}
23
+
24
+ if s.respond_to? :specification_version then
25
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
26
+ s.specification_version = 2
27
+
28
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
29
+ else
30
+ end
31
+ else
32
+ end
33
+ end
@@ -0,0 +1,140 @@
1
+ require "rubygems"
2
+ require "ruby-debug"
3
+ require "spec"
4
+ require "mocha"
5
+ require "pp"
6
+
7
+ require File.join(File.dirname(__FILE__), 'spec_helper')
8
+
9
+ describe "MissingT" do
10
+ before do
11
+ @missing_t = MissingT.new
12
+ @es_translations = {"es"=>
13
+ {"zoo"=>{"elephant"=>"elefante", "bear"=>"oso", "lion"=>"leon", "bee" => "abeja"},
14
+ "lamp"=>"lampa",
15
+ "book"=>"libro",
16
+ "handkerchief"=>"panuelo",
17
+ "pen" => "boli"}}
18
+ @fr_translations = {"fr"=>
19
+ {"zoo"=>{"elephant"=>"elephant", "bear"=>"ours", "lion"=>"lion", "wasp" => "guepe"},
20
+ "lamp"=>"lampe",
21
+ "book"=>"livre",
22
+ "handkerchief"=>"mouchoir",
23
+ "mother" => "mere"}}
24
+
25
+ @other_es_translations = { "es" => {"zoo" => {}}}
26
+ @yet_other_es_translations = { "es" => {"zoo" => {"monkey" => "mono", "horse" => "caballo"}}}
27
+ end
28
+
29
+ describe "adding translations" do
30
+ before do
31
+ @missing_t.add_translations(@es_translations)
32
+ end
33
+
34
+ it "should pick up the new translations" do
35
+ @missing_t.translations.should == @es_translations
36
+ end
37
+
38
+ it "should correctly merge different translations" do
39
+ @missing_t.add_translations(@fr_translations)
40
+ @missing_t["fr"]["zoo"].should have_key("wasp")
41
+ @missing_t["fr"].should have_key("mother")
42
+ @missing_t["es"]["zoo"].should have_key("bee")
43
+ end
44
+
45
+ it "should not overwrite translations keys" do
46
+ @missing_t.add_translations(@other_es_translations)
47
+ @missing_t["es"]["zoo"].should have_key("bear")
48
+ @missing_t["es"]["zoo"].should have_key("bee")
49
+ end
50
+
51
+ it "should add the new translations even if they contain keys already in the translations hash" do
52
+ @missing_t.add_translations(@yet_other_es_translations)
53
+ @missing_t["es"]["zoo"].should have_key("monkey")
54
+ @missing_t["es"]["zoo"].should have_key("bear")
55
+ end
56
+
57
+ end
58
+
59
+ describe "hashification" do
60
+ before do
61
+ queries = ["zoo.bee", "zoo.departments.food", "zoo.departments.qa", "lamp", "mother", "mother.maiden_name"]
62
+ @queries_hash = @missing_t.hashify(queries)
63
+ @h = { "fr" => { "book" => "livre", "zoo" => {"elephant" => "elephant"} } }
64
+ end
65
+
66
+ it "should find a nested key and return it" do
67
+ @h.should have_nested_key('fr.zoo.elephant')
68
+ @h.should have_nested_key('fr.book')
69
+ end
70
+
71
+ it "should return false when it does not have a nested key" do
72
+ @h.should_not have_nested_key('fr.zoo.seal')
73
+ @h.should_not have_nested_key('xxx')
74
+ end
75
+
76
+ it "an empty hash should not have any nested keys" do
77
+ {}.should_not have_nested_key(:puppy)
78
+ end
79
+
80
+ it "should turn strings to hash keys along their separators (dots)" do
81
+ ["zoo", "lamp", "mother"].all? { |k| @queries_hash.key?(k) }.should == true
82
+ ["bee", "departments"].all? { |k| @queries_hash["zoo"].key?(k) }.should == true
83
+ @queries_hash["zoo"]["departments"].should have_key("food")
84
+ @queries_hash["zoo"]["departments"].should have_key("qa")
85
+ end
86
+ end
87
+
88
+ describe "extracting i18n queries" do
89
+ before do
90
+ content = <<-EOS
91
+ <div class="title_gray"><span><%= I18n.t("anetcom.member.projects.new.page_title") %></span></div>
92
+ <%= submit_tag I18n.t('anetcom.member.projects.new.create_project'), :class => 'button' %>
93
+ <%= link_to I18n.t("tog_headlines.admin.publish"), publish_admin_headlines_story_path(story), :class => 'button' %>
94
+ :html => {:title => I18n.t("tog_social.sharing.share_with", :name => shared.name)}
95
+ EOS
96
+ $stubba = Mocha::Central.new
97
+ @missing_t.stubs(:get_content_of_file_with_i18n_queries).returns(content)
98
+ end
99
+
100
+ it "should extract the I18n queries correctly when do" do
101
+ i18n_queries = @missing_t.extract_i18n_queries(nil)
102
+ i18n_queries.should == ["anetcom.member.projects.new.page_title", "anetcom.member.projects.new.create_project", "tog_headlines.admin.publish", "tog_social.sharing.share_with"]
103
+ end
104
+
105
+ end
106
+
107
+ describe "finding missing translations" do
108
+ before do
109
+ @t_queries = { :fake_file => ["mother", "zoo.bee", "zoo.wasp", "pen"] }
110
+ $stubba = Mocha::Central.new
111
+ @missing_t.stubs(:translations).returns(@fr_translations.merge(@es_translations))
112
+ # @missing_t.stubs(:collect_translation_queries).returns(@t_queries)
113
+ end
114
+
115
+ it "should return true if it has a translation given in the I18n form" do
116
+ @missing_t.has_translation?("fr", "zoo.wasp").should == true
117
+ @missing_t.has_translation?("es", "pen").should == true
118
+ end
119
+
120
+ it "should return false if it does not have a translation given in the I18n form" do
121
+ @missing_t.has_translation?("fr", "zoo.bee").should == false
122
+ @missing_t.has_translation?("es", "mother").should == false
123
+ end
124
+
125
+ it "should correctly get missing translations for a spec. language" do
126
+ miss_entries = @missing_t.get_missing_translations(@t_queries, "fr").map{ |e| e[1] }.flatten
127
+ miss_entries.should include("fr.pen")
128
+ miss_entries.should include("fr.zoo.bee")
129
+ end
130
+
131
+ it "should correctly get missing translations" do
132
+ miss_entries = @missing_t.get_missing_translations(@t_queries).map{ |e| e[1] }.flatten
133
+ miss_entries.should include("fr.zoo.bee")
134
+ miss_entries.should include("fr.pen")
135
+ miss_entries.should include("es.zoo.wasp")
136
+ miss_entries.should include("es.mother")
137
+ end
138
+ end
139
+
140
+ end
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "..", "lib", "missing_t")
@@ -0,0 +1,6 @@
1
+ require 'spec/rake/spectask'
2
+
3
+ desc "Run all specs"
4
+ Spec::Rake::SpecTask.new('spec') do |t|
5
+ t.spec_files = FileList['spec/**/*spec.rb']
6
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: balinterdi-missing_t
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Balint Erdi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-03 00:00:00 -08:00
13
+ default_executable: missing_t
14
+ dependencies: []
15
+
16
+ description: See all the missing I18n translations in your Rails project
17
+ email: balint.erdi@gmail.com
18
+ executables:
19
+ - missing_t
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - bin/missing_t
24
+ - lib/missing_t.rb
25
+ - README.rdoc
26
+ - tasks/missing_t.rake
27
+ files:
28
+ - bin/missing_t
29
+ - lib/missing_t.rb
30
+ - Manifest
31
+ - Rakefile
32
+ - README.rdoc
33
+ - spec/missing_t_spec.rb
34
+ - spec/spec_helper.rb
35
+ - tasks/missing_t.rake
36
+ - missing_t.gemspec
37
+ has_rdoc: true
38
+ homepage: http://github.com/balinterdi/missing_t
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --line-numbers
42
+ - --inline-source
43
+ - --title
44
+ - Missing_t
45
+ - --main
46
+ - README.rdoc
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "1.2"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project: missing_t
64
+ rubygems_version: 1.2.0
65
+ signing_key:
66
+ specification_version: 2
67
+ summary: See all the missing I18n translations in your Rails project
68
+ test_files: []
69
+