i15r 0.4.4 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +7 -0
- data/Gemfile.lock +1 -1
- data/README.markdown +31 -12
- data/bin/i15r +39 -4
- data/lib/i15r.rb +88 -3
- data/lib/i15r/console_printer.rb +13 -0
- data/lib/i15r/file_reader.rb +7 -0
- data/lib/i15r/file_writer.rb +7 -0
- data/lib/i15r/pattern_matcher.rb +154 -3
- data/lib/i15r/version.rb +2 -2
- data/spec/i15r_spec.rb +73 -71
- data/spec/pattern_matcher_spec.rb +190 -14
- data/spec/spec_helper.rb +37 -2
- data/spec/support/internationalize_matcher.rb +20 -0
- metadata +14 -47
- data/VERSION +0 -1
- data/lib/i15r/base.rb +0 -123
- data/lib/i15r/pattern_matchers/base.rb +0 -40
- data/lib/i15r/pattern_matchers/erb.rb +0 -10
- data/lib/i15r/pattern_matchers/erb/rails_helper_matcher.rb +0 -75
- data/lib/i15r/pattern_matchers/erb/tag_attribute_matcher.rb +0 -26
- data/lib/i15r/pattern_matchers/erb/tag_content_matcher.rb +0 -40
- data/lib/i15r/pattern_matchers/haml.rb +0 -9
- data/lib/i15r/pattern_matchers/haml/rails_helper_matcher.rb +0 -83
- data/lib/i15r/pattern_matchers/haml/tag_content_matcher.rb +0 -52
- data/spec/erb/rails_helper_matcher_spec.rb +0 -81
- data/spec/erb/tag_attribute_matcher_spec.rb +0 -19
- data/spec/erb/tag_content_matcher_spec.rb +0 -43
- data/spec/haml/rails_helper_matcher_spec.rb +0 -92
- data/spec/haml/tag_content_matcher_spec.rb +0 -82
data/.travis.yml
ADDED
data/Gemfile.lock
CHANGED
data/README.markdown
CHANGED
@@ -1,27 +1,31 @@
|
|
1
|
-
# I15r
|
1
|
+
# I15r ![Build Status](https://api.travis-ci.org/balinterdi/i15r.png)
|
2
|
+
|
2
3
|
|
3
4
|
## Summary
|
4
5
|
|
5
|
-
I15r (Internationalizer) searches for all the non-i18n texts in your views in
|
6
|
+
I15r (Internationalizer) searches for all the non-i18n texts in your views in
|
7
|
+
the given files/directory and replaces them with I18n messages. The message
|
8
|
+
string is based on the file in which the text was found and the text itself
|
9
|
+
that was replaced.
|
6
10
|
|
7
11
|
E.g
|
8
12
|
|
9
13
|
(in file app/views/users/new.html.erb)
|
10
14
|
<label for="user-name">Name</label>
|
11
15
|
<input type="text" id="user-name" name="user[name]" />
|
12
|
-
|
16
|
+
|
13
17
|
will be replaced by:
|
14
18
|
|
15
19
|
(in file app/views/users/new.html.erb)
|
16
20
|
<label for="user-name"><%= I18n.t("users.new.name") %></label>
|
17
21
|
<input type="text" id="user-name" name="user[name]" />
|
18
|
-
|
19
|
-
and
|
22
|
+
|
23
|
+
and
|
20
24
|
|
21
25
|
(in file app/views/member/users/edit.html.erb)
|
22
26
|
<label for="user-name">Name</label>
|
23
27
|
<input type="text" id="user-name" name="user[name]" />
|
24
|
-
|
28
|
+
|
25
29
|
will be replaced by
|
26
30
|
|
27
31
|
(in file app/views/member/users/edit.html.erb)
|
@@ -33,7 +37,11 @@ It can process erb and haml files.
|
|
33
37
|
## Installation
|
34
38
|
|
35
39
|
gem install i15r
|
36
|
-
|
40
|
+
|
41
|
+
or put the following in your Gemfile:
|
42
|
+
|
43
|
+
gem 'i15r', '~> 0.4.4'
|
44
|
+
|
37
45
|
## Usage
|
38
46
|
|
39
47
|
### Convert a single file
|
@@ -43,18 +51,18 @@ It can process erb and haml files.
|
|
43
51
|
### Convert all files in a directory (deep search)
|
44
52
|
|
45
53
|
i15r path/leading/to/directory
|
46
|
-
|
54
|
+
|
47
55
|
All files with an erb or haml suffix in that directory or somewhere in the hierarchy below will be converted.
|
48
56
|
|
49
57
|
### Dry run
|
50
58
|
|
51
59
|
By default, i15r overwrites all the source files with the i18n message strings it generates. If you first want to see what would be replaced, you should do:
|
52
60
|
|
53
|
-
i15r app/views/users -
|
61
|
+
i15r app/views/users -n
|
54
62
|
|
55
63
|
or
|
56
64
|
|
57
|
-
i15r app/views/users --
|
65
|
+
i15r app/views/users --dry-run
|
58
66
|
|
59
67
|
### Custom prefix
|
60
68
|
|
@@ -74,10 +82,21 @@ The file will then contain:
|
|
74
82
|
|
75
83
|
## Disclaimer (sort of)
|
76
84
|
|
77
|
-
Please note that this is an early version mainly built up of examples I've come
|
85
|
+
Please note that this is an early version mainly built up of examples I've come
|
86
|
+
through doing client work. I am pretty sure there are a number of cases which
|
87
|
+
i15r -at the moment- does not handle well (or at all). If you find such an
|
88
|
+
example, please [let me know][issue_tracker] or if you feel motivated, submit a
|
89
|
+
patch. Oh, yes, to prevent unwanted changes to your view files, you should use
|
90
|
+
a SCM (that goes without saying, of course) and probably use the --pretend
|
91
|
+
option.
|
78
92
|
|
79
93
|
[issue_tracker]: http://github.com/balinterdi/i15r/issues
|
80
94
|
|
81
95
|
## Licensing, contribution
|
82
96
|
|
83
|
-
The source code of this gem can be found at
|
97
|
+
The source code of this gem can be found at
|
98
|
+
[http://github.com/balinterdi/i15r/](http://github.com/balinterdi/i15r/). It is
|
99
|
+
released under the MIT-LICENSE, so you can basically do anything with it.
|
100
|
+
However, if you think your modifications only make the tool better, please send
|
101
|
+
a pull request or patch and I will consider merging in your changes. Any
|
102
|
+
suggestions or feedback are welcome to <balint@balinterdi.com>.
|
data/bin/i15r
CHANGED
@@ -3,9 +3,44 @@
|
|
3
3
|
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
|
4
4
|
|
5
5
|
require 'i15r'
|
6
|
+
require 'i15r/version'
|
7
|
+
require 'i15r/file_reader'
|
8
|
+
require 'i15r/file_writer'
|
9
|
+
require 'i15r/console_printer'
|
10
|
+
require 'optparse'
|
6
11
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
12
|
+
def parse_options(args)
|
13
|
+
options = {}
|
14
|
+
opts = OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: ruby i15r.rb [options] <path_to_internationalize>"
|
16
|
+
opts.on("--prefix PREFIX",
|
17
|
+
"Apply PREFIX to generated I18n messages instead of deriving it from the path") do |prefix|
|
18
|
+
options[:prefix] = prefix
|
19
|
+
end
|
20
|
+
opts.on("-n", "--dry-run", "Do not write the files, just show the diff") do
|
21
|
+
options[:dry_run] = true
|
22
|
+
end
|
23
|
+
opts.on("--no-default", "Do not insert the replaced string as the :default in the I18n string") do
|
24
|
+
options[:add_default] = false
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
28
|
+
puts opts
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on_tail("--version", "Show version") do
|
33
|
+
puts I15R::VERSION
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.parse!(args)
|
39
|
+
options
|
11
40
|
end
|
41
|
+
|
42
|
+
@i15r = I15R.new(I15R::FileReader.new,
|
43
|
+
I15R::FileWriter.new,
|
44
|
+
I15R::ConsolePrinter.new,
|
45
|
+
parse_options(ARGV))
|
46
|
+
@i15r.internationalize!(ARGV[-1])
|
data/lib/i15r.rb
CHANGED
@@ -1,6 +1,91 @@
|
|
1
|
-
require 'i15r/base'
|
2
1
|
require 'i15r/pattern_matcher'
|
3
|
-
require 'i15r/version'
|
4
2
|
|
5
|
-
|
3
|
+
class I15R
|
4
|
+
class AppFolderNotFound < Exception; end
|
5
|
+
|
6
|
+
class Config
|
7
|
+
def initialize(config)
|
8
|
+
@options = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def prefix
|
12
|
+
@options.fetch(:prefix, nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
def dry_run?
|
16
|
+
@options.fetch(:dry_run, false)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_default
|
20
|
+
@options.fetch(:add_default, true)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :config
|
25
|
+
|
26
|
+
def initialize(reader, writer, printer, config={})
|
27
|
+
@reader = reader
|
28
|
+
@writer = writer
|
29
|
+
@printer = printer
|
30
|
+
@config = I15R::Config.new(config)
|
31
|
+
end
|
32
|
+
|
33
|
+
def config=(hash)
|
34
|
+
@config = I15R::Config.new(hash)
|
35
|
+
end
|
36
|
+
|
37
|
+
def file_path_to_message_prefix(file)
|
38
|
+
segments = File.expand_path(file).split('/').reject { |segment| segment.empty? }
|
39
|
+
subdir = %w(views helpers controllers models).find do |app_subdir|
|
40
|
+
segments.index(app_subdir)
|
41
|
+
end
|
42
|
+
if subdir.nil?
|
43
|
+
raise AppFolderNotFound, "No app. subfolders were found to determine prefix. Path is #{File.expand_path(file)}"
|
44
|
+
end
|
45
|
+
first_segment_index = segments.index(subdir) + 1
|
46
|
+
file_name_without_extensions = segments.last.split('.').first
|
47
|
+
if file_name_without_extensions and file_name_without_extensions[0] == '_'
|
48
|
+
file_name_without_extensions = file_name_without_extensions[1..-1]
|
49
|
+
end
|
50
|
+
path_segments = segments.slice(first_segment_index...-1)
|
51
|
+
if path_segments.empty?
|
52
|
+
file_name_without_extensions
|
53
|
+
else
|
54
|
+
"#{path_segments.join('.')}.#{file_name_without_extensions}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def internationalize_file(path)
|
59
|
+
text = @reader.read(path)
|
60
|
+
prefix = config.prefix || file_path_to_message_prefix(path)
|
61
|
+
template_type = path[/(?:.*)\.(.*)$/, 1]
|
62
|
+
@printer.println("#{path}:")
|
63
|
+
@printer.println("")
|
64
|
+
i18ned_text = sub_plain_strings(text, prefix, template_type.to_sym)
|
65
|
+
@writer.write(path, i18ned_text) unless config.dry_run?
|
66
|
+
end
|
67
|
+
|
68
|
+
def display_indented_header(prefix)
|
69
|
+
puts "en:"
|
70
|
+
prefix_parts = prefix.split(".").each_with_index do |p, i|
|
71
|
+
p = "#{p}:"
|
72
|
+
#TODO: perhaps " "*i is simpler
|
73
|
+
(0..i).each { |i| p = " " + p }
|
74
|
+
puts "#{p}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def sub_plain_strings(text, prefix, file_type)
|
79
|
+
pm = I15R::PatternMatcher.new(prefix, file_type, :add_default => config.add_default)
|
80
|
+
transformed_text = pm.run(text) do |old_line, new_line|
|
81
|
+
@printer.print_diff(old_line, new_line)
|
82
|
+
end
|
83
|
+
transformed_text + "\n"
|
84
|
+
end
|
85
|
+
|
86
|
+
def internationalize!(path)
|
87
|
+
#TODO: Indicate if we're running in dry-run mode
|
88
|
+
files = File.directory?(path) ? Dir.glob("#{path}/**/*.{erb,haml}") : [path]
|
89
|
+
files.each { |file| internationalize_file(file) }
|
90
|
+
end
|
6
91
|
end
|
data/lib/i15r/pattern_matcher.rb
CHANGED
@@ -1,3 +1,154 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
class I15R
|
2
|
+
class PatternMatcher
|
3
|
+
HAML_SYMBOLS = ["%", "#", "{", "}", "(", ")", ".", "_", "-"]
|
4
|
+
PATTERNS = {
|
5
|
+
:erb => [
|
6
|
+
/<%=\s*link_to\s+(?<title>['"].+?['"])/,
|
7
|
+
/<%=.*label(_tag)?[^,]+?(?<label-title>(['"].+?['"]|:[[:alnum:]_]+))[^,]+%>.*$/,
|
8
|
+
/<%=.*label(_tag)?.*?,\s*(?<label-title>(['"].+?['"]|:[[:alnum:]_]+))/,
|
9
|
+
/<%=.*submit(_tag)?\s+(?<submit-text>(['"].+?['"]|:[[:alnum:]_]+))/,
|
10
|
+
/>(?<tag-content>[[:space:][:alnum:][:punct:]]+?)<\//,
|
11
|
+
/<a\s+title=['"](?<link-title>.+?)['"]/,
|
12
|
+
/^\s*(?<pre-tag-text>[[:alnum:]]+[[:alnum:][:space:][:punct:]]*?)</
|
13
|
+
],
|
14
|
+
:haml => [
|
15
|
+
/=.*link_to\s+(?<title>['"].+?['"]),/,
|
16
|
+
/=.*label(_tag)?[^,]+?(?<label-title>(['"].+?['"]|:[[:alnum:]_]+))[^,]*$/,
|
17
|
+
/=.*label(_tag)?.*?,\s*(?<label-title>(['"].+?['"]|:[[:alnum:]_]+))/,
|
18
|
+
/=.*submit(_tag)?\s+(?<submit-text>(['"].+?['"]|:[[:alnum:]_]+))/,
|
19
|
+
%r{^\s*(?<content>[[:space:][:alnum:]'/(),]+)$},
|
20
|
+
%r{^\s*[[#{HAML_SYMBOLS.join('')}][:alnum:]]+?\{.+?\}\s+(?<content>.+)$},
|
21
|
+
%r{^\s*[[#{HAML_SYMBOLS.join('')}][:alnum:]]+?\(.+?\)\s+(?<content>.+)$},
|
22
|
+
%r{^\s*[[#{(HAML_SYMBOLS - ['{', '}', '(', ')']).join('')}][:alnum:]]+?\s+(?<content>.+)$}
|
23
|
+
]
|
24
|
+
}
|
25
|
+
|
26
|
+
def initialize(prefix, file_type, options={})
|
27
|
+
@prefix = prefix
|
28
|
+
@file_type = file_type
|
29
|
+
transformer_class = self.class.const_get("#{file_type.to_s.capitalize}Transformer")
|
30
|
+
@transformer = transformer_class.new(options[:add_default])
|
31
|
+
end
|
32
|
+
|
33
|
+
def translation_key(text)
|
34
|
+
#TODO: downcase does not work properly for accented chars, like 'Ú', see function in ActiveSupport that deals with this
|
35
|
+
#TODO: [:punct:] would be nice but it includes _ which we don't want to remove
|
36
|
+
key = text.strip.downcase.gsub(/[\s\/]+/, '_').gsub(/[!?.,:"';()]/, '')
|
37
|
+
"#{@prefix}.#{key}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def run(text)
|
41
|
+
lines = text.split("\n")
|
42
|
+
new_lines = lines.map do |line|
|
43
|
+
new_line = line
|
44
|
+
PATTERNS[@file_type].detect do |pattern|
|
45
|
+
if m = pattern.match(line)
|
46
|
+
m.names.each do |group_name|
|
47
|
+
if /\w/.match(m[group_name])
|
48
|
+
new_line = @transformer.transform(m, m[group_name], line, translation_key(m[group_name]))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
if block_given? and line != new_line
|
54
|
+
yield line, new_line
|
55
|
+
end
|
56
|
+
new_line
|
57
|
+
end
|
58
|
+
new_lines.join("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
class Transformer
|
62
|
+
def initialize(add_default)
|
63
|
+
@add_default = add_default
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def i18n_string(key, original)
|
68
|
+
if @add_default
|
69
|
+
if original.to_s[0] == ':'
|
70
|
+
original = original.to_s[1..-1]
|
71
|
+
end
|
72
|
+
unless original[0] == "'" or original[0] == '"'
|
73
|
+
original = %("#{original}")
|
74
|
+
end
|
75
|
+
%(I18n.t("#{key}", :default => #{original}))
|
76
|
+
else
|
77
|
+
%(I18n.t("#{key}"))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class ErbTransformer < Transformer
|
83
|
+
|
84
|
+
def transform(match_data, match, line, translation_key)
|
85
|
+
return line if line.match /\bt\(/
|
86
|
+
if match_data.to_s.index("<%")
|
87
|
+
line.gsub(match, i18n_string(translation_key, match))
|
88
|
+
else
|
89
|
+
line.gsub(match, "<%= #{i18n_string(translation_key, match)} %>")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
class HamlTransformer < Transformer
|
96
|
+
|
97
|
+
def transform(match_data, match, line, translation_key)
|
98
|
+
return line if line.match /\bt\(/
|
99
|
+
leading_whitespace = line[/^(\s+)/, 1]
|
100
|
+
no_leading_whitespace = if leading_whitespace
|
101
|
+
line[leading_whitespace.size..-1]
|
102
|
+
else
|
103
|
+
line
|
104
|
+
end
|
105
|
+
if ['/', '-'].include?(no_leading_whitespace[0])
|
106
|
+
return line
|
107
|
+
end
|
108
|
+
|
109
|
+
# Space can only occur in haml markup in an attribute list
|
110
|
+
# enclosed in { } or ( ). If the first segment has { or (
|
111
|
+
# we are still in the markup and need to go on to find the beginning
|
112
|
+
# of the string to be replaced
|
113
|
+
i = 0
|
114
|
+
haml_segment = true
|
115
|
+
attribute_list_start = nil
|
116
|
+
segments = no_leading_whitespace.split(/\s+/)
|
117
|
+
while haml_segment
|
118
|
+
s = segments[i]
|
119
|
+
if attribute_list_start
|
120
|
+
attribute_list_end = [')', '}'].detect { |sym| s.index(sym) }
|
121
|
+
if attribute_list_end
|
122
|
+
haml_segment = false
|
123
|
+
end
|
124
|
+
else
|
125
|
+
attribute_list_start = ['(', '{'].detect { |sym| s.index(sym) }
|
126
|
+
unless attribute_list_start
|
127
|
+
haml_segment = false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
i += 1
|
131
|
+
end
|
132
|
+
|
133
|
+
until_first_whitespace = segments[0...i].join(' ')
|
134
|
+
if HAML_SYMBOLS.any? { |sym| until_first_whitespace.index(sym) }
|
135
|
+
haml_markup = until_first_whitespace
|
136
|
+
content = segments[i..-1].join(' ')
|
137
|
+
if haml_markup[-1] == '='
|
138
|
+
haml_markup += ' '
|
139
|
+
else
|
140
|
+
haml_markup += '= '
|
141
|
+
end
|
142
|
+
else
|
143
|
+
haml_markup = ''
|
144
|
+
content = no_leading_whitespace
|
145
|
+
content.insert(0, '= ') unless content[0] == '='
|
146
|
+
end
|
147
|
+
|
148
|
+
new_line = (leading_whitespace or '') + haml_markup + content
|
149
|
+
new_line.gsub(match.gsub(/\s+$/, ''), i18n_string(translation_key, match))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
data/lib/i15r/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = "0.
|
1
|
+
class I15R
|
2
|
+
VERSION = "0.5.0"
|
3
3
|
end
|