i15r 0.4.4 → 0.5.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/.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 
|
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
|