missing_t 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile.lock +5 -1
- data/Guardfile +1 -1
- data/README.markdown +33 -25
- data/bin/missing_t +1 -1
- data/lib/missing_t.rb +44 -59
- data/missing_t.gemspec +1 -0
- data/spec/missing_t_spec.rb +14 -101
- metadata +20 -4
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
missing_t (0.3.
|
4
|
+
missing_t (0.3.2)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
@@ -18,7 +18,10 @@ GEM
|
|
18
18
|
rspec (~> 2.11)
|
19
19
|
listen (0.7.2)
|
20
20
|
lumberjack (1.0.2)
|
21
|
+
metaclass (0.0.1)
|
21
22
|
method_source (0.8.1)
|
23
|
+
mocha (0.13.3)
|
24
|
+
metaclass (~> 0.0.1)
|
22
25
|
pry (0.9.11.4)
|
23
26
|
coderay (~> 1.0.5)
|
24
27
|
method_source (~> 0.8)
|
@@ -43,6 +46,7 @@ DEPENDENCIES
|
|
43
46
|
guard (~> 1.5.4)
|
44
47
|
guard-rspec (~> 2.3.1)
|
45
48
|
missing_t!
|
49
|
+
mocha (~> 0.13.3)
|
46
50
|
rake (~> 10.0.3)
|
47
51
|
rb-fsevent (~> 0.9.1)
|
48
52
|
rspec (~> 2.12.0)
|
data/Guardfile
CHANGED
data/README.markdown
CHANGED
@@ -1,43 +1,51 @@
|
|
1
1
|
# Missing T
|
2
|
+
[![Code Climate](https://codeclimate.com/github/balinterdi/missing_t.png)](https://codeclimate.com/github/balinterdi/missing_t)
|
2
3
|
|
3
|
-
Missing T provides
|
4
|
+
Missing T provides a quick way to see which I18n message strings lack their translations in your Ruby project.
|
4
5
|
|
5
6
|
## Installation
|
6
7
|
|
7
|
-
Missing T comes packaged as a gem,
|
8
|
+
Missing T comes packaged as a gem, you just have to add it to your Gemfile:
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
Also, if you prefer to use it as a plugin to Rails project, you can simply do the following:
|
12
|
-
|
13
|
-
$ ./script/plugin install git://github.com/balinterdi/missing_t.git
|
10
|
+
gem 'missing_t', '~> 0.3.1'
|
14
11
|
|
15
12
|
## Running
|
16
13
|
|
17
14
|
To find all the messages without translations, you have to be in your project directory and then launch missing_t in the most simple way imaginable:
|
18
15
|
|
19
|
-
$ missing_t
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
16
|
+
$ bundle exec missing_t
|
17
|
+
|
18
|
+
All messages that don't have translations will be outputted in a format directly pastable to your locale files:
|
19
|
+
|
20
|
+
fr:
|
21
|
+
users:
|
22
|
+
name:
|
23
|
+
city_of_residence:
|
24
|
+
travels:
|
25
|
+
es:
|
26
|
+
users:
|
27
|
+
age:
|
28
|
+
city_of_residence:
|
29
|
+
events:
|
30
|
+
venue:
|
31
|
+
|
32
|
+
|
31
33
|
__NOTE__ If no language code is provided, the script will determine which languages need to have translations by gathering all language codes in the localization files and assuming that if there is at least one translation defined for a language then all translations should be defined for it.
|
32
34
|
|
33
|
-
If you wish to see all
|
35
|
+
If you wish to see all missing translations for a certain language, just provide its language code as a parameter:
|
34
36
|
|
35
|
-
$ missing_t fr
|
37
|
+
$ bundle exec missing_t fr
|
38
|
+
|
39
|
+
In this case only missing translations in the provided language will be printed:
|
36
40
|
|
37
|
-
|
41
|
+
fr:
|
42
|
+
users:
|
43
|
+
name:
|
44
|
+
city_of_residence:
|
45
|
+
travels:
|
38
46
|
|
39
|
-
|
47
|
+
## Epilogue
|
40
48
|
|
41
|
-
|
49
|
+
Released under the MIT license.
|
42
50
|
|
43
|
-
|
51
|
+
2009-2013 Balint Erdi
|
data/bin/missing_t
CHANGED
@@ -20,7 +20,7 @@ def print_hash(h, level)
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
missing_t = MissingT.new
|
23
|
+
missing_t = MissingT.new(MissingT::FileReader.new)
|
24
24
|
missing_t.parse_options(ARGV)
|
25
25
|
missing_message_strings = missing_t.find_missing_translations(ARGV.first).values.map { |ms| hashify(ms) }
|
26
26
|
|
data/lib/missing_t.rb
CHANGED
@@ -4,15 +4,6 @@ require "ostruct"
|
|
4
4
|
require "forwardable"
|
5
5
|
|
6
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
7
|
# idea snatched from deep_merge in Rails source code
|
17
8
|
def deep_safe_merge(other_hash)
|
18
9
|
self.merge(other_hash) do |key, oldval, newval|
|
@@ -52,16 +43,20 @@ end
|
|
52
43
|
|
53
44
|
class MissingT
|
54
45
|
|
55
|
-
|
46
|
+
class FileReader
|
47
|
+
def read(file)
|
48
|
+
open(File.expand_path(file), "r") do |f|
|
49
|
+
yield f.read
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
VERSION = "0.3.2"
|
56
55
|
|
57
56
|
include Helpers
|
58
|
-
extend Forwardable
|
59
|
-
def_delegators :@translations, :[]
|
60
57
|
|
61
|
-
|
62
|
-
|
63
|
-
def initialize
|
64
|
-
@translations = Hash.new
|
58
|
+
def initialize(reader)
|
59
|
+
@reader = reader
|
65
60
|
end
|
66
61
|
|
67
62
|
def parse_options(args)
|
@@ -88,57 +83,48 @@ class MissingT
|
|
88
83
|
opts.parse!(args)
|
89
84
|
end
|
90
85
|
|
91
|
-
|
92
|
-
# because attr_reader :translations
|
93
|
-
# does not seem to be stubbable
|
94
|
-
def translations
|
95
|
-
@translations
|
96
|
-
end
|
97
|
-
|
98
|
-
def add_translations(trs)
|
99
|
-
translations.deep_safe_merge!(trs)
|
100
|
-
end
|
101
|
-
|
102
|
-
def collect_translations
|
86
|
+
def translation_keys
|
103
87
|
locales_pathes = ["config/locales/**/*.yml", "vendor/plugins/**/config/locales/**/*yml", "vendor/plugins/**/locale/**/*yml"]
|
104
|
-
locales_pathes.
|
88
|
+
locales_pathes.each_with_object({}) do |path, translations|
|
105
89
|
Dir.glob(path) do |file|
|
106
|
-
|
90
|
+
t = open(file) { |f| YAML.load(f.read) }
|
91
|
+
translations.deep_safe_merge!(t)
|
107
92
|
end
|
108
93
|
end
|
109
94
|
end
|
110
95
|
|
111
|
-
def translations_in_file(yaml_file)
|
112
|
-
open(yaml_file) { |f| YAML.load(f.read) }
|
113
|
-
end
|
114
|
-
|
115
96
|
def files_with_i18n_queries
|
116
97
|
if path = @options.path
|
117
98
|
path = path[0...-1] if path[-1..-1] == '/'
|
118
|
-
[
|
99
|
+
[
|
100
|
+
Dir.glob("#{path}/**/*.erb"),
|
101
|
+
Dir.glob("#{path}/**/*.haml"),
|
102
|
+
Dir.glob("#{path}/**/*.rb")
|
103
|
+
]
|
119
104
|
else
|
120
|
-
[
|
121
|
-
|
122
|
-
|
105
|
+
[
|
106
|
+
Dir.glob("app/**/*.erb"),
|
107
|
+
Dir.glob("app/**/*.haml"),
|
108
|
+
Dir.glob("app/**/models/**/*.rb"),
|
109
|
+
Dir.glob("app/**/controllers/**/*.rb"),
|
110
|
+
Dir.glob("app/**/helpers/**/*.rb")
|
111
|
+
]
|
123
112
|
end.flatten
|
124
113
|
end
|
125
114
|
|
126
|
-
def get_content_of_file_with_i18n_queries(file)
|
127
|
-
f = open(File.expand_path(file), "r")
|
128
|
-
content = f.read()
|
129
|
-
f.close()
|
130
|
-
content
|
131
|
-
end
|
132
|
-
|
133
115
|
def extract_i18n_queries(file)
|
134
116
|
i18n_query_pattern = /[^\w]+(?:I18n\.translate|I18n\.t|translate|t)\s*\((.*?)[,\)]/
|
135
117
|
i18n_query_no_parens_pattern = /[^\w]+(?:I18n\.translate|I18n\.t|translate|t)\s+(['"])(.*?)\1/
|
136
|
-
|
137
|
-
|
138
|
-
|
118
|
+
|
119
|
+
@reader.read(file) do |content|
|
120
|
+
([]).tap do |i18n_message_strings|
|
121
|
+
i18n_message_strings << content.scan(i18n_query_pattern).map { |match| match[0].gsub(/['"\s]/, '') }
|
122
|
+
i18n_message_strings << content.scan(i18n_query_no_parens_pattern).map { |match| match[1].gsub(/['"\s]/, '') }
|
123
|
+
end.flatten
|
124
|
+
end
|
139
125
|
end
|
140
126
|
|
141
|
-
def
|
127
|
+
def translation_queries
|
142
128
|
files_with_i18n_queries.each_with_object({}) do |file, queries|
|
143
129
|
queries_in_file = extract_i18n_queries(file)
|
144
130
|
if queries_in_file.any?
|
@@ -148,18 +134,17 @@ class MissingT
|
|
148
134
|
#TODO: remove duplicate queries across files
|
149
135
|
end
|
150
136
|
|
151
|
-
def has_translation?(lang, query)
|
152
|
-
t = translations
|
137
|
+
def has_translation?(keys, lang, query)
|
153
138
|
i18n_label(lang, query).split('.').each do |segment|
|
154
|
-
return false unless segment =~ /#\{.*\}/ or (
|
155
|
-
|
139
|
+
return false unless segment =~ /#\{.*\}/ or (keys.respond_to?(:key?) and keys.key?(segment))
|
140
|
+
keys = keys[segment]
|
156
141
|
end
|
157
142
|
true
|
158
143
|
end
|
159
144
|
|
160
|
-
def get_missing_translations(queries, languages)
|
145
|
+
def get_missing_translations(keys, queries, languages)
|
161
146
|
languages.each_with_object({}) do |lang, missing|
|
162
|
-
get_missing_translations_for_lang(queries, lang).each do |file, queries|
|
147
|
+
get_missing_translations_for_lang(keys, queries, lang).each do |file, queries|
|
163
148
|
missing[file] ||= []
|
164
149
|
missing[file].concat(queries).uniq!
|
165
150
|
end
|
@@ -167,14 +152,14 @@ class MissingT
|
|
167
152
|
end
|
168
153
|
|
169
154
|
def find_missing_translations(lang=nil)
|
170
|
-
|
171
|
-
get_missing_translations(
|
155
|
+
ts = translation_keys
|
156
|
+
get_missing_translations(translation_keys, translation_queries, lang ? [lang] : ts.keys)
|
172
157
|
end
|
173
158
|
|
174
159
|
private
|
175
|
-
def get_missing_translations_for_lang(queries, lang)
|
160
|
+
def get_missing_translations_for_lang(keys, queries, lang)
|
176
161
|
queries.map do |file, queries_in_file|
|
177
|
-
queries_with_no_translation = queries_in_file.select { |q| !has_translation?(lang, q) }
|
162
|
+
queries_with_no_translation = queries_in_file.select { |q| !has_translation?(keys, lang, q) }
|
178
163
|
if queries_with_no_translation.empty?
|
179
164
|
nil
|
180
165
|
else
|
data/missing_t.gemspec
CHANGED
@@ -23,4 +23,5 @@ Gem::Specification.new do |gem|
|
|
23
23
|
gem.add_development_dependency 'guard', ['~> 1.5.4']
|
24
24
|
gem.add_development_dependency 'guard-rspec', ['~> 2.3.1']
|
25
25
|
gem.add_development_dependency 'rb-fsevent', ['~> 0.9.1']
|
26
|
+
gem.add_development_dependency 'mocha', ['~> 0.13.3']
|
26
27
|
end
|
data/spec/missing_t_spec.rb
CHANGED
@@ -1,18 +1,20 @@
|
|
1
|
-
require
|
2
|
-
require "spec"
|
3
|
-
require "mocha"
|
4
|
-
|
5
|
-
require File.join(File.dirname(__FILE__), 'spec_helper')
|
1
|
+
require 'spec_helper'
|
6
2
|
|
7
3
|
# use mocha for mocking instead of
|
8
4
|
# Rspec's own mock framework
|
9
|
-
|
5
|
+
RSpec.configure do |config|
|
10
6
|
config.mock_with :mocha
|
11
7
|
end
|
12
8
|
|
9
|
+
class ContentReader
|
10
|
+
def read(content)
|
11
|
+
yield content
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
13
15
|
describe "MissingT" do
|
14
16
|
before do
|
15
|
-
@missing_t = MissingT.new
|
17
|
+
@missing_t = MissingT.new(ContentReader.new)
|
16
18
|
@es_translations = {"es"=>
|
17
19
|
{"zoo"=>{"elephant"=>"elefante", "bear"=>"oso", "lion"=>"leon", "bee" => "abeja"},
|
18
20
|
"lamp"=>"lampa",
|
@@ -30,75 +32,7 @@ describe "MissingT" do
|
|
30
32
|
@yet_other_es_translations = { "es" => {"zoo" => {"monkey" => "mono", "horse" => "caballo"}}}
|
31
33
|
end
|
32
34
|
|
33
|
-
describe "adding translations" do
|
34
|
-
before do
|
35
|
-
@missing_t.add_translations(@es_translations)
|
36
|
-
end
|
37
|
-
|
38
|
-
it "should pick up the new translations" do
|
39
|
-
@missing_t.translations.should == @es_translations
|
40
|
-
end
|
41
|
-
|
42
|
-
it "should correctly merge different translations" do
|
43
|
-
@missing_t.add_translations(@fr_translations)
|
44
|
-
@missing_t["fr"]["zoo"].should have_key("wasp")
|
45
|
-
@missing_t["fr"].should have_key("mother")
|
46
|
-
@missing_t["es"]["zoo"].should have_key("bee")
|
47
|
-
end
|
48
|
-
|
49
|
-
it "should not overwrite translations keys" do
|
50
|
-
@missing_t.add_translations(@other_es_translations)
|
51
|
-
@missing_t["es"]["zoo"].should have_key("bear")
|
52
|
-
@missing_t["es"]["zoo"].should have_key("bee")
|
53
|
-
end
|
54
|
-
|
55
|
-
it "should add the new translations even if they contain keys already in the translations hash" do
|
56
|
-
@missing_t.add_translations(@yet_other_es_translations)
|
57
|
-
@missing_t["es"]["zoo"].should have_key("monkey")
|
58
|
-
@missing_t["es"]["zoo"].should have_key("bear")
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
describe "hashification" do
|
64
|
-
before do
|
65
|
-
queries = ["zoo.bee", "zoo.departments.food", "zoo.departments.qa", "lamp", "mother", "mother.maiden_name"]
|
66
|
-
@queries_hash = @missing_t.hashify(queries)
|
67
|
-
@h = { "fr" => { "book" => "livre", "zoo" => {"elephant" => "elephant"} } }
|
68
|
-
end
|
69
|
-
|
70
|
-
it "should find a nested key and return it" do
|
71
|
-
@h.should have_nested_key('fr.zoo.elephant')
|
72
|
-
@h.should have_nested_key('fr.book')
|
73
|
-
end
|
74
|
-
|
75
|
-
it "should return false when it does not have a nested key" do
|
76
|
-
@h.should_not have_nested_key('fr.zoo.seal')
|
77
|
-
@h.should_not have_nested_key('xxx')
|
78
|
-
end
|
79
|
-
|
80
|
-
it "an empty hash should not have any nested keys" do
|
81
|
-
{}.should_not have_nested_key(:puppy)
|
82
|
-
end
|
83
|
-
|
84
|
-
it "should turn strings to hash keys along their separators (dots)" do
|
85
|
-
["zoo", "lamp", "mother"].all? { |k| @queries_hash.key?(k) }.should == true
|
86
|
-
["bee", "departments"].all? { |k| @queries_hash["zoo"].key?(k) }.should == true
|
87
|
-
@queries_hash["zoo"]["departments"].should have_key("food")
|
88
|
-
@queries_hash["zoo"]["departments"].should have_key("qa")
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
35
|
describe "the i18n query extracion" do
|
93
|
-
before do
|
94
|
-
metaclass = class << @missing_t; self; end
|
95
|
-
metaclass.instance_eval do
|
96
|
-
define_method :get_content_of_file_with_i18n_queries do |content|
|
97
|
-
content
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
36
|
it "should correctly extract the I18n.t type of messages" do
|
103
37
|
content = <<-EOS
|
104
38
|
<div class="title_gray"><span><%= I18n.t("anetcom.member.projects.new.page_title") %></span></div>
|
@@ -165,7 +99,6 @@ describe "MissingT" do
|
|
165
99
|
end
|
166
100
|
|
167
101
|
it "should find and correctly extract a dynamic key translation message" do
|
168
|
-
# @missing_t.stubs(:get_content_of_file_with_i18n_queries).returns(content)
|
169
102
|
content = %q(<div class="title_gray"><span><%= I18n.t("mycompany.welcome.#{key}") %></span></div>)
|
170
103
|
@missing_t.extract_i18n_queries(content).should == [%q(mycompany.welcome.#{key})]
|
171
104
|
end
|
@@ -175,38 +108,18 @@ describe "MissingT" do
|
|
175
108
|
describe "finding missing translations" do
|
176
109
|
before do
|
177
110
|
@t_queries = { :fake_file => ["mother", "zoo.bee", "zoo.wasp", "pen"] }
|
178
|
-
@missing_t.stubs(:
|
179
|
-
|
180
|
-
end
|
181
|
-
|
182
|
-
it "should return true if it has a translation given in the I18n form" do
|
183
|
-
@missing_t.has_translation?("fr", "zoo.wasp").should == true
|
184
|
-
@missing_t.has_translation?("es", "pen").should == true
|
185
|
-
end
|
186
|
-
|
187
|
-
it "should return false if it does not have a translation given in the I18n form" do
|
188
|
-
@missing_t.has_translation?("fr", "zoo.bee").should == false
|
189
|
-
@missing_t.has_translation?("es", "mother").should == false
|
190
|
-
end
|
191
|
-
|
192
|
-
describe "of dynamic message strings" do
|
193
|
-
it "should return true if it has a translation that matches the fix parts" do
|
194
|
-
@missing_t.has_translation?("fr", %q(zoo.#{animal})).should == true
|
195
|
-
end
|
196
|
-
|
197
|
-
it "should return false if it does not have a translation that matches all the fix parts" do
|
198
|
-
@missing_t.has_translation?("fr", %q(household.#{animal})).should == false
|
199
|
-
end
|
111
|
+
@missing_t.stubs(:translation_keys).returns(@fr_translations.merge(@es_translations))
|
112
|
+
@missing_t.stubs(:translation_queries).returns(@t_queries)
|
200
113
|
end
|
201
114
|
|
202
|
-
it "should correctly get missing translations for a
|
203
|
-
miss_entries = @missing_t.
|
115
|
+
it "should correctly get missing translations for a specific language" do
|
116
|
+
miss_entries = @missing_t.find_missing_translations("fr").map{ |e| e[1] }.flatten
|
204
117
|
miss_entries.should include("fr.pen")
|
205
118
|
miss_entries.should include("fr.zoo.bee")
|
206
119
|
end
|
207
120
|
|
208
121
|
it "should correctly get missing translations" do
|
209
|
-
miss_entries = @missing_t.
|
122
|
+
miss_entries = @missing_t.find_missing_translations.map{ |e| e[1] }.flatten
|
210
123
|
miss_entries.should include("fr.zoo.bee")
|
211
124
|
miss_entries.should include("fr.pen")
|
212
125
|
miss_entries.should include("es.zoo.wasp")
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: missing_t
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-03-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -91,6 +91,22 @@ dependencies:
|
|
91
91
|
- - ~>
|
92
92
|
- !ruby/object:Gem::Version
|
93
93
|
version: 0.9.1
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: mocha
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 0.13.3
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 0.13.3
|
94
110
|
description: Finds all the missing i18n translations in your Rails project
|
95
111
|
email:
|
96
112
|
- balint.erdi@gmail.com
|
@@ -148,7 +164,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
148
164
|
version: '0'
|
149
165
|
segments:
|
150
166
|
- 0
|
151
|
-
hash: -
|
167
|
+
hash: -840801129156003864
|
152
168
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
169
|
none: false
|
154
170
|
requirements:
|
@@ -157,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
173
|
version: '0'
|
158
174
|
segments:
|
159
175
|
- 0
|
160
|
-
hash: -
|
176
|
+
hash: -840801129156003864
|
161
177
|
requirements: []
|
162
178
|
rubyforge_project:
|
163
179
|
rubygems_version: 1.8.23
|