i18n_template 0.0.1

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.
Files changed (57) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile.rails23_r187 +8 -0
  3. data/Gemfile.rails30_r193 +8 -0
  4. data/Gemfile.rails31_r193 +8 -0
  5. data/README.md +250 -0
  6. data/Rakefile +1 -0
  7. data/bin/i18n_template +5 -0
  8. data/i18n_template.gemspec +26 -0
  9. data/lib/i18n_template.rb +28 -0
  10. data/lib/i18n_template/document.rb +460 -0
  11. data/lib/i18n_template/extractor.rb +4 -0
  12. data/lib/i18n_template/extractor/base.rb +44 -0
  13. data/lib/i18n_template/extractor/gettext.rb +127 -0
  14. data/lib/i18n_template/extractor/plain.rb +43 -0
  15. data/lib/i18n_template/extractor/yaml.rb +53 -0
  16. data/lib/i18n_template/handler.rb +61 -0
  17. data/lib/i18n_template/node.rb +74 -0
  18. data/lib/i18n_template/railtie.rb +7 -0
  19. data/lib/i18n_template/runner.rb +61 -0
  20. data/lib/i18n_template/runner/base.rb +11 -0
  21. data/lib/i18n_template/runner/extract_phrases.rb +70 -0
  22. data/lib/i18n_template/tasks.rb +2 -0
  23. data/lib/i18n_template/translation.rb +62 -0
  24. data/lib/i18n_template/translator.rb +5 -0
  25. data/lib/i18n_template/translator/i18n.rb +24 -0
  26. data/lib/i18n_template/version.rb +3 -0
  27. data/test/abstract_unit.rb +11 -0
  28. data/test/document_test.rb +316 -0
  29. data/test/fixtures/handling_if_blocks.yml +23 -0
  30. data/test/fixtures/ignored_markup.yml +15 -0
  31. data/test/fixtures/incorrect_node_markup.yml +17 -0
  32. data/test/fixtures/nested_nodes.yml +16 -0
  33. data/test/fixtures/nested_wrapped_text.yml +15 -0
  34. data/test/fixtures/phrase_fully_ignored.yml +14 -0
  35. data/test/fixtures/phrase_with_embed_words_and_scriptlet.yml +17 -0
  36. data/test/fixtures/phrase_with_single_char_to_ignore.yml +19 -0
  37. data/test/fixtures/replacing_br_with_newline.yml +15 -0
  38. data/test/fixtures/skipping_ignored_blocks.yml +15 -0
  39. data/test/fixtures/spans_as_phrases.yml +18 -0
  40. data/test/fixtures/table.yml +35 -0
  41. data/test/fixtures/text_with_braces.yml +17 -0
  42. data/test/fixtures/text_with_brackets.yml +17 -0
  43. data/test/fixtures/wrapped_key_propagation.yml +15 -0
  44. data/test/fixtures/wrapping_eval_blocks.yml +17 -0
  45. data/test/fixtures_rendering_test.rb +46 -0
  46. data/test/inline_rendering_test.rb +27 -0
  47. data/test/support/i18n_test_case_helper.rb +12 -0
  48. data/test/templates/_footer.html.erb +3 -0
  49. data/test/templates/greeting.html.erb +1 -0
  50. data/test/templates/layouts/application.html.erb +5 -0
  51. data/test/templates/users/_account.html.erb +3 -0
  52. data/test/templates/users/_profile.html.erb +6 -0
  53. data/test/templates/users/index.html.erb +5 -0
  54. data/test/templates/users/show.html.erb +4 -0
  55. data/test/templates_rendering_test.rb +81 -0
  56. data/test/translate_test.rb +72 -0
  57. metadata +156 -0
@@ -0,0 +1,11 @@
1
+ module I18nTemplate
2
+ module Runner
3
+ ##
4
+ # Base runner class
5
+ class Base
6
+ def initialize(options)
7
+ @options = options
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ require 'i18n_template/extractor'
2
+
3
+ module I18nTemplate
4
+ module Runner
5
+ class ExtractPhrases < Base
6
+
7
+ # register runner
8
+ I18nTemplate.runners << self
9
+
10
+ class << self
11
+ def command
12
+ 'extract_phrases'
13
+ end
14
+
15
+ def description
16
+ 'extract phrases for translations'
17
+ end
18
+
19
+ def default_options
20
+ @default_options ||= I18nTemplate.extractors.inject({}) { |result, klass|
21
+ result.merge!(klass.default_options)
22
+ }
23
+ end
24
+
25
+ def add_options!(parser, options)
26
+ formats = I18nTemplate.extractors.map { |klass| klass.format }
27
+
28
+ parser.on(
29
+ "--format #{formats.join('|')}",
30
+ "translation format (default #{default_options[:format]})"
31
+ ) { |v| options[:format] = v }
32
+
33
+ parser.on(
34
+ "--po-root PO ROOT",
35
+ "root directly for po files (default #{default_options[:po_root]})"
36
+ ) { |v| options[:po_root] = v }
37
+
38
+ parser.on(
39
+ "--glob GLOB",
40
+ "template files glob (default #{default_options[:glob].join(',')})"
41
+ ) { |v| (options[:glob] ||= []) << v }
42
+
43
+ parser.on(
44
+ "--textdomain TEXTDOMAIN",
45
+ "gettext textdomain (default #{default_options[:textdomain]})"
46
+ ) { |v| options[:textdomain] = v }
47
+
48
+ parser.on(
49
+ "--output-file FILE",
50
+ "output file (default #{default_options[:output_file]})"
51
+ ) { |v| options[:output_file] = v }
52
+
53
+ parser.on(
54
+ "--locales-root DIRECTORY",
55
+ "locales directory (default #{default_options[:locales_root]})"
56
+ ) { |v| options[:locales_root] = v }
57
+ end
58
+ end
59
+
60
+ def run
61
+ paths = Dir[*@options[:glob]]
62
+
63
+ extractor = I18nTemplate.extractors.detect { |klass| klass.format == @options[:format] }
64
+ extractor or abort "Unknown extract format: #{@options[:format]}"
65
+ extractor.new(@options).call(paths)
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,2 @@
1
+ namespace :i18n_template do
2
+ end
@@ -0,0 +1,62 @@
1
+ module I18nTemplate
2
+ module Translation
3
+ class << self
4
+ CLOSE_TAG_BEGIN_PATTERN = /
5
+ (?=<\/)(.*?)$ # match any string that begins with <\/ characters as 1
6
+ /x.freeze
7
+
8
+ WRAPPER_OR_VARIABLE_PATTERN = /(
9
+ \{\} # empty braces
10
+ | # or
11
+ \{([^\}]+)\} # match any data except close brace in braces as 2
12
+ | # or
13
+ \[(\/?)(\d+)\] # optional match back-slash character as 3 and digits as 4 in brackets
14
+ )/x.freeze
15
+
16
+ T9N_CLEANUP_PATTERN = /\s+(i18n_wrapper="\d+"|i18n="\w")/x.freeze
17
+
18
+ # translate phrase and replace placeholders and variables
19
+ def translate(key, wrappers, vars)
20
+ # map each wrapper to open and close tag
21
+ # e.g '<a><b><c>bla</d></e>' -> [ '<a><b><c>bla', '</d></e>' ]
22
+ wrappers = wrappers.collect do |w|
23
+ w =~ CLOSE_TAG_BEGIN_PATTERN ? [w[0, w.size-$1.size], $1] : ['', '']
24
+ end
25
+
26
+ phrase = (I18nTemplate.translator.call(key) || key).dup
27
+
28
+ # replaces {variable name} or [digits] or [\digits]
29
+ # with wrappers and variables
30
+ phrase.gsub!(WRAPPER_OR_VARIABLE_PATTERN) do |s|
31
+ case s[0, 1]
32
+ when '{' then s == '{}' ? vars[''] : vars[$2]
33
+ when '[' then (wrappers[$4.to_i] || [])['/' == $3 ? 1 : 0]
34
+ end
35
+ end
36
+
37
+ # replace nl with break line
38
+ phrase.gsub!("[nl]", "<br />")
39
+
40
+ # replace unescaped characters
41
+ phrase.gsub!("[lsb]", "[")
42
+ phrase.gsub!("[rsb]", "]")
43
+ phrase.gsub!("[lcb]", "{")
44
+ phrase.gsub!("[rcb]", "}")
45
+ phrase.gsub!("[ns]", "#")
46
+
47
+ # remove i18n attributes. E.g:
48
+ # i18n_wrapper='100'
49
+ # i18n="i"
50
+ phrase.gsub!(T9N_CLEANUP_PATTERN, '')
51
+
52
+ if phrase.respond_to?(:html_safe)
53
+ # return html_safe phrase
54
+ phrase.html_safe
55
+ else
56
+ # return pure string
57
+ phrase
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ module I18nTemplate
2
+ module Translator
3
+ autoload :I18n, 'i18n_template/translator/i18n'
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ require "i18n"
2
+
3
+ module I18nTemplate
4
+ module Translator
5
+ ##
6
+ # Standard i18n translator for i18n_template
7
+ class I18n
8
+
9
+ # Special symbol for separator
10
+ NO_SEPARATOR = [0x10308].pack('U*').freeze
11
+
12
+ def self.call(phrase)
13
+ self.new.call(phrase)
14
+ end
15
+
16
+ def call(phrase)
17
+ ::I18n.translate(phrase, {
18
+ :default => "~#{phrase}",
19
+ :separator => NO_SEPARATOR
20
+ })
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module I18nTemplate
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,11 @@
1
+ $KCODE='u' if RUBY_VERSION < '1.9'
2
+
3
+ $:.unshift File.expand_path('../../lib', __FILE__)
4
+ require 'i18n_template'
5
+
6
+ require 'action_controller'
7
+ require 'action_view/test_case'
8
+
9
+ require 'support/i18n_test_case_helper'
10
+
11
+ ActionView::Template.register_template_handler(:erb, I18nTemplate::Handler.new)
@@ -0,0 +1,316 @@
1
+ # encoding: UTF-8
2
+ require 'abstract_unit'
3
+
4
+ class DocumentTest < Test::Unit::TestCase
5
+ include I18nTestCaseHelper
6
+
7
+ def test_folds
8
+ source =<<-DATA
9
+ <body>
10
+ <h2>hello</h2>
11
+ <div>hi <%= user.last_post.comments.size %> world</div>
12
+ <!-- <div></div> -->
13
+ </body>
14
+ DATA
15
+
16
+ document = I18nTemplate::Document.new(source)
17
+
18
+ document.send :fold_special_tags!
19
+
20
+ assert_equal(2, document.folds.size)
21
+ assert_equal('<!-- <div></div> -->', document.folds[0])
22
+ assert_equal('<%= user.last_post.comments.size %>', document.folds[1])
23
+ assert_equal(<<-DATA, document.source)
24
+ <body>
25
+ <h2>hello</h2>
26
+ <div>hi ≤1:eval≥ world</div>
27
+ ≤0:ignore≥
28
+ </body>
29
+ DATA
30
+
31
+ document.send :unfold_special_tags!
32
+
33
+ assert_equal(<<-DATA, document.source)
34
+ <body>
35
+ <h2>hello</h2>
36
+ <div>hi <%= user.last_post.comments.size %> world</div>
37
+ <!-- <div></div> -->
38
+ </body>
39
+ DATA
40
+ end
41
+
42
+ def test_preprocess
43
+ source =<<-DATA
44
+ <body>
45
+ <% current_year = Time.now.year %>
46
+ <span i18n="p">hello</span>
47
+ <h2>Dashboard</h2>
48
+ <div>Posts count: <%= current_user.posts.count %></div>
49
+ <div>Click<a href="#">here</a></div>
50
+ </body>
51
+ DATA
52
+
53
+ document = I18nTemplate::Document.new(source)
54
+ document.preprocess!
55
+
56
+
57
+ assert_equal([
58
+ 'hello',
59
+ 'Dashboard',
60
+ 'Posts count: {current user posts count}',
61
+ 'Click[1]here[/1]',
62
+ 'here'
63
+ ], document.phrases)
64
+
65
+
66
+ assert_equal(<<-DATA, document.source)
67
+ <body>
68
+ <% current_year = Time.now.year %>
69
+ <span i18n="p" i18n_phrase="hello">hello</span>
70
+ <h2 i18n_phrase="Dashboard">Dashboard</h2>
71
+ <div i18n_phrase="Posts count: {current user posts count}">Posts count: <i18n_variable name="current user posts count"><%= current_user.posts.count %></i18n_variable></div>
72
+ <div i18n_phrase="Click[1]here[/1]">Click<a href=\"#\" i18n_wrapper="1" i18n_phrase="here">here</a></div>
73
+ </body>
74
+ DATA
75
+ end
76
+
77
+ def test_process
78
+ source =<<-DATA
79
+ <body>
80
+ <% current_year = Time.now.year %>
81
+ <span i18n="p">hello</span>
82
+ <h2>Dashboard</h2>
83
+ <div>Posts count: <%= current_user.posts.count %></div>
84
+ <div>Click<a href="#">here</a></div>
85
+ </body>
86
+ DATA
87
+
88
+ document = I18nTemplate::Document.new(source)
89
+ document.process!
90
+
91
+ assert_equal(<<-DATA, document.source)
92
+ <body>
93
+ <% current_year = Time.now.year %>
94
+ <span><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate("hello", i18n_wrappers, i18n_variables) %></span>
95
+ <h2><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate("Dashboard", i18n_wrappers, i18n_variables) %></h2>
96
+ <div><%- i18n_variables = {}; i18n_wrappers = [] -%><%- i18n_variables['current user posts count'] = capture do -%><%= current_user.posts.count %><%- end -%><%= ::I18nTemplate::Translation.translate("Posts count: {current user posts count}", i18n_wrappers, i18n_variables) %></div>
97
+ <div><%- i18n_variables = {}; i18n_wrappers = [] -%><%- i18n_wrappers[1] = capture do -%><a href="#" i18n_wrapper="1"><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate("here", i18n_wrappers, i18n_variables) %></a><%- end -%><%= ::I18nTemplate::Translation.translate("Click[1]here[/1]", i18n_wrappers, i18n_variables) %></div>
98
+ </body>
99
+ DATA
100
+
101
+ end
102
+
103
+ def test_render_partial
104
+ source =<<-DATA
105
+ <div><%= render :partial => 'menu' %></div>
106
+ DATA
107
+
108
+ document = I18nTemplate::Document.new(source)
109
+ document.process!
110
+
111
+ assert_equal(<<-DATA, document.source)
112
+ <div><%= render :partial => 'menu' %></div>
113
+ DATA
114
+ end
115
+
116
+ def test_br
117
+ source =<<-DATA
118
+ <label for="merchant_address">Merchant address <br/>(required)</label>
119
+ DATA
120
+
121
+ document = I18nTemplate::Document.new(source)
122
+ document.process!
123
+
124
+ assert_equal(<<-DATA, document.source)
125
+ <label for="merchant_address"><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate("Merchant address [nl](required)", i18n_wrappers, i18n_variables) %></label>
126
+ DATA
127
+ end
128
+
129
+ def test_good_nested
130
+ source =<<-DATA
131
+ <div><span><span>This is</span></span> <%= message %></div>
132
+ DATA
133
+
134
+ document = I18nTemplate::Document.new(source)
135
+ document.process!
136
+
137
+ assert_equal(<<-DATA, document.source)
138
+ <div><%- i18n_variables = {}; i18n_wrappers = [] -%><%- i18n_wrappers[1] = capture do -%><span i18n_wrapper="1"><span></span></span><%- end -%><%- i18n_variables['message'] = capture do -%><%= message %><%- end -%><%= ::I18nTemplate::Translation.translate("[1]This is[/1] {message}", i18n_wrappers, i18n_variables) %></div>
139
+ DATA
140
+ end
141
+
142
+ def test_bad_nested
143
+ source =<<-DATA
144
+ <div><span><span>This</span>is</span><%= message %></div>
145
+ DATA
146
+ document = I18nTemplate::Document.new(source)
147
+ document.process!
148
+
149
+ assert_equal(<<-DATA, document.source)
150
+ <div><%- i18n_variables = {}; i18n_wrappers = [] -%><%- i18n_wrappers[1] = capture do -%><span i18n_wrapper="1"><span></span></span><%- end -%><%- i18n_variables['message'] = capture do -%><%= message %><%- end -%><%= ::I18nTemplate::Translation.translate("NNODE[1]is[/1]{message}", i18n_wrappers, i18n_variables) %></div>
151
+ DATA
152
+ end
153
+
154
+ def test_extra_open_tags
155
+ source =<<-DATA
156
+ <div>
157
+ <h2>hello</h2>
158
+ <p>
159
+ DATA
160
+
161
+ document = I18nTemplate::Document.new(source)
162
+ document.process!
163
+
164
+ assert_equal([
165
+ ], document.warnings)
166
+
167
+ result =<<-DATA
168
+ <div>
169
+ <h2><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate(\"hello\", i18n_wrappers, i18n_variables) %></h2>
170
+ <p>
171
+ </p></div>
172
+ DATA
173
+
174
+ assert_equal(result.strip, document.source)
175
+ end
176
+
177
+ def test_extra_close_tags
178
+ source =<<-DATA
179
+ <div>
180
+ <h2>hello</h2>
181
+ </div>
182
+ </div>
183
+ DATA
184
+
185
+ document = I18nTemplate::Document.new(source)
186
+ document.process!
187
+
188
+ assert_equal([
189
+ "[SOURCE:4]: EXTRA CLOSING TAG:div, UP:ROOT",
190
+ "[SOURCE:4]: EXTRA CLOSING TAG:div, UP:ROOT"
191
+ ], document.warnings)
192
+
193
+ assert_equal(<<-DATA, document.source)
194
+ <div>
195
+ <h2><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate(\"hello\", i18n_wrappers, i18n_variables) %></h2>
196
+ </div>
197
+
198
+ DATA
199
+ end
200
+
201
+ def test_select_tag
202
+ source =<<-DATA
203
+ <div>
204
+ <select name="company">
205
+ <option value="volvo">Volvo</option>
206
+ <option value="saab">Saab</option>
207
+ <option value="mercedes">Mercedes</option>
208
+ <option value="audi">Audi</option>
209
+ </select>
210
+ </div>
211
+ DATA
212
+
213
+ document = I18nTemplate::Document.new(source)
214
+ document.process!
215
+
216
+ assert_equal([], document.warnings)
217
+
218
+ assert_equal(<<-DATA, document.source)
219
+ <div>
220
+ <select name="company">
221
+ <option value="volvo">Volvo</option>
222
+ <option value="saab">Saab</option>
223
+ <option value="mercedes">Mercedes</option>
224
+ <option value="audi">Audi</option>
225
+ </select>
226
+ </div>
227
+ DATA
228
+ end
229
+
230
+ def test_select_tag_form_helper
231
+ source =<<-DATA
232
+ <div>
233
+ <label for="people">People</label>
234
+ <%= select_tag "people", options_from_collection_for_select(@people, "id", "name") %>
235
+ </div>
236
+ DATA
237
+
238
+ #--------------------------------------------------
239
+ # puts "#"*80
240
+ # require 'i18n_template/processor'
241
+ # processor = I18nTemplate::Processor.new(source)
242
+ # processor.process!
243
+ # puts processor.template
244
+ # p processor.phrases
245
+ #--------------------------------------------------
246
+
247
+ document = I18nTemplate::Document.new(source)
248
+ document.process!
249
+
250
+ assert_equal([], document.warnings)
251
+ assert_equal([
252
+ 'People'
253
+ ], document.phrases)
254
+ assert_equal(<<-DATA, document.source)
255
+ <div>
256
+ <label for="people"><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate("People", i18n_wrappers, i18n_variables) %></label>
257
+ <%= select_tag "people", options_from_collection_for_select(@people, "id", "name") %>
258
+ </div>
259
+ DATA
260
+ end
261
+
262
+
263
+ def test_nbsp_html_entity
264
+ source =<<-DATA
265
+ <div style="clear:both">&nbsp; &#169;</div>
266
+ DATA
267
+
268
+ #--------------------------------------------------
269
+ # puts "#"*80
270
+ # require 'i18n_template/processor'
271
+ # processor = I18nTemplate::Processor.new(source)
272
+ # processor.process!
273
+ # puts processor.template
274
+ # p processor.phrases
275
+ #
276
+ #--------------------------------------------------
277
+ document = I18nTemplate::Document.new(source)
278
+ document.process!
279
+
280
+ assert_equal([], document.warnings)
281
+ assert_equal([], document.phrases)
282
+ assert_equal(<<-DATA, document.source)
283
+ <div style="clear:both">&nbsp; &#169;</div>
284
+ DATA
285
+ end
286
+
287
+ def test_text_with_braces
288
+ source =<<-DATA
289
+ <div>hello {user}</div>
290
+ DATA
291
+
292
+ document = I18nTemplate::Document.new(source)
293
+ document.process!
294
+
295
+ assert_equal([], document.warnings)
296
+ assert_equal(['hello [lcb]user[rcb]'], document.phrases)
297
+ assert_equal(<<-DATA, document.source)
298
+ <div><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate(\"hello [lcb]user[rcb]\", i18n_wrappers, i18n_variables) %></div>
299
+ DATA
300
+ end
301
+
302
+ def test_text_with_brackets
303
+ source =<<-DATA
304
+ <div>hello [1] user [/1]</div>
305
+ DATA
306
+
307
+ document = I18nTemplate::Document.new(source)
308
+ document.process!
309
+
310
+ assert_equal([], document.warnings)
311
+ assert_equal(['hello [lsb]1[rsb] user [lsb]/1[rsb]'], document.phrases)
312
+ assert_equal(<<-DATA, document.source)
313
+ <div><%- i18n_variables = {}; i18n_wrappers = [] -%><%= ::I18nTemplate::Translation.translate(\"hello [lsb]1[rsb] user [lsb]/1[rsb]\", i18n_wrappers, i18n_variables) %></div>
314
+ DATA
315
+ end
316
+ end