i15r 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - jruby
6
+ - rbx-19mode
7
+ script: "bundle exec rspec spec"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- i15r (0.4.4)
4
+ i15r (0.5.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
@@ -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 the given files/directory and replaces them with I18n messages. The message string is based on the file in which the text was found and the text itself that was replaced.
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 -p
61
+ i15r app/views/users -n
54
62
 
55
63
  or
56
64
 
57
- i15r app/views/users --pretend
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 through doing client work. I am pretty sure there are a number of cases which i15r -at the moment- does not handle well (or at all). If you find such an example, please [let me know][issue_tracker] or if you feel motivated, submit a patch. Oh, yes, to prevent unwanted changes to your view files, you should use a SCM (that goes without saying, of course) and probably use the --pretend option.
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 [http://github.com/balinterdi/i15r/](http://github.com/balinterdi/i15r/). It is released under the MIT-LICENSE, so you can basically do anything with it. However, if you think your modifications only make the tool better, please send a pull request or patch and I will consider merging in your changes. Any suggestions or feedback are welcome to <balint@balinterdi.com>.
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
- @i15r = I15R::Base.new
8
- @i15r.instance_eval do
9
- parse_options(ARGV)
10
- internationalize!(ARGV[-1])
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])
@@ -1,6 +1,91 @@
1
- require 'i15r/base'
2
1
  require 'i15r/pattern_matcher'
3
- require 'i15r/version'
4
2
 
5
- module I15R
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
@@ -0,0 +1,13 @@
1
+ class I15R
2
+ class ConsolePrinter
3
+ def println(text)
4
+ puts text
5
+ end
6
+
7
+ def print_diff(old_row, new_row)
8
+ puts "- #{old_row}"
9
+ puts "+ #{new_row}"
10
+ puts
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ class I15R
2
+ class FileReader
3
+ def read(file)
4
+ File.read(File.expand_path(file))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class I15R
2
+ class FileWriter
3
+ def write(path, content)
4
+ open(File.expand_path(path), "w") { |f| f.write(content) }
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,154 @@
1
- require 'i15r/pattern_matchers/base'
2
- require 'i15r/pattern_matchers/erb'
3
- require 'i15r/pattern_matchers/haml'
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
@@ -1,3 +1,3 @@
1
- module I15R
2
- VERSION = "0.4.4"
1
+ class I15R
2
+ VERSION = "0.5.0"
3
3
  end