i18n_template 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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