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,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.*.lock
4
+ pkg/*
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rails", "2.3.14"
6
+
7
+ gem "ansi"
8
+ gem "turn"
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rails", "3.0.11"
6
+
7
+ gem "ansi"
8
+ gem "turn"
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rails", "3.1.3"
6
+
7
+ gem "ansi"
8
+ gem "turn"
@@ -0,0 +1,250 @@
1
+ # I18nTemplate
2
+
3
+ ## Main Feature
4
+
5
+ Just compare regulare rails view internationalization:
6
+
7
+ <html>
8
+ <body>
9
+ <% current_year = Time.now.year %>
10
+ <span><%= t('hello') %></span>
11
+ <h2><%= t('Dashboard') </h2>
12
+ <div><%= t('Posts count:') %><%= current_user.posts.count %></div>
13
+ <div><%= t('Click') %><a href="#"><%= t('here') %></a></div>
14
+ ...
15
+ </body>
16
+ </html>
17
+
18
+ with i18n template internationalization:
19
+
20
+ <html>
21
+ <body>
22
+ <% current_year = Time.now.year %>
23
+ <span i18n="p">hello</span>
24
+ <h2>Dashboard</h2>
25
+ <div>Posts count: <%= current_user.posts.count %></div>
26
+ <div>Click<a href="#">here</a></div>
27
+ ...
28
+ </body>
29
+ </html>
30
+
31
+ Nice?
32
+
33
+ ## How it Works
34
+
35
+ It convert *on the fly* regular erb template to another erb template. For above example this is something like:
36
+
37
+ <html>
38
+ <body>
39
+ <% current_year = Time.now.year %>
40
+ <span>
41
+ <%- i18n_variables = {}; i18n_wrappers = [] -%>
42
+ <%= ::I18nTemplate::Translation.translate("hello", i18n_wrappers, i18n_variables) %>
43
+ </span>
44
+ <h2>
45
+ <%- i18n_variables = {}; i18n_wrappers = [] -%>
46
+ <%= ::I18nTemplate::Translation.translate("Dashboard", i18n_wrappers, i18n_variables) %>
47
+ </h2>
48
+ <div>
49
+ <%- i18n_variables = {}; i18n_wrappers = [] -%>
50
+ <%- i18n_variables['current user posts count'] = capture do -%>
51
+ <%= current_user.posts.count %>
52
+ <%- end -%>
53
+ <%= ::I18nTemplate::Translation.translate("Posts count: {current user posts count}",
54
+ i18n_wrappers, i18n_variables) %>
55
+ </div>
56
+ <div>
57
+ <%- i18n_variables = {}; i18n_wrappers = [] -%>
58
+ <%- i18n_wrappers[1] = capture do -%>
59
+ <a href="#" i18n_wrapper="1">
60
+ <%- i18n_variables = {}; i18n_wrappers = [] -%>
61
+ <%= ::I18nTemplate::Translation.translate("here", i18n_wrappers, i18n_variables) %>
62
+ </a>
63
+ <%- end -%>
64
+ <%= ::I18nTemplate::Translation.translate("Click[1]here[/1]", i18n_wrappers, i18n_variables) %>
65
+ </div>
66
+ </body>
67
+ </html>
68
+
69
+ Translation phrases (keys):
70
+
71
+ * _hello_
72
+ * _Dashboard_
73
+ * _Posts count: {current user posts count}_
74
+ * _Click[1]here[/1]_
75
+
76
+ ## Description
77
+
78
+ I18nTemplate is made to extract phrases and translate html/xhtml/xml document or erb templates.
79
+ Currently the it can work with (x)html documents.
80
+ Translation is done by modify the original template (on the fly) to be translated on erb execution time.
81
+
82
+ ## Semantics
83
+
84
+ The engine is leveraging the HTML document semantics.
85
+ As we know HTML document element can contain : block elements and/or inline elements.
86
+ The engine has the following parsing rules, based on what kind of children a parent element contains:
87
+
88
+ * block element containing only block elements - is named a parent element, and is ignored by the engine;
89
+ * block element containing only inline elements - is named phrase, while every inline element is named a word;
90
+ * inline element containing other inline elements - is also a word;
91
+ * any other variation - is considered a broken element, which should be one of there above.
92
+
93
+ ### Markup
94
+
95
+ Additionally for the sake of best practices and optimiztion the following rules take place:
96
+
97
+ * the following elements as considered block elements by the engine
98
+ * usual block : `blockquote p div h1 h2 h3 h4 h5 h6 li dd dt`
99
+ * inline elements : `td th a legend label title caption option optgroup button`
100
+ * the following elements, and their content, will be ignored by the engine:
101
+ * html elements: `select style script`
102
+ * non-breaking space: `&nbsp;`
103
+ * erb scriptlets: `<% <%=`
104
+ * html comments: `<!-- -->`
105
+ * xhtml doctype: `<!DOCTYPE`
106
+ * additional best practices are added to translate content inside such tags
107
+
108
+ In order to fix a broken elements next elements/attributes can be added to the html document to resolve engine misunderstanding:
109
+
110
+ * `<i18n>content</i18n>` - mark invisible for parser content for internationalization
111
+ * `<... i18n="i" ...>content<...>` - (ignore) ignore element content internationalization
112
+ * `<... i18n="p" ...>content<...>` - (phrase) explicitly enable content internationalization
113
+ * `<... i18n="s" ...>content<...>` - (subphrase) mark element content as sub-phrase for parent element phrase
114
+
115
+ ## Translation
116
+
117
+ ### Brackets
118
+
119
+ [1]Hello World[/1]
120
+
121
+ ### Braces
122
+
123
+ Example
124
+
125
+ Hello { user name }
126
+
127
+ * `<%= @user_name %>` as `{user name}`
128
+ * `<%= user_name %>` as `{user name}`
129
+ * `<%= @post.comments.count %>` as `{post comments count}`
130
+
131
+ ## Using with Rails (2.3.x 3.x.x)
132
+
133
+ $ gem install i18n_template
134
+
135
+ require 'i18n_template'
136
+
137
+ ActionView::Template.register_template_handler(:erb, I18nTemplate::Handler.new)
138
+
139
+ ### Set another phrase translator:
140
+
141
+ I18nTemplate.phrase_translator = lambda { |phrase| Google.translate(phrase) }
142
+
143
+ ### More template internationalize control
144
+
145
+ Assume we don't want to internationalize admin view templates.
146
+
147
+ class MyI18nTemplateHandler < I18nTemplate::Handler
148
+ def internationalize?(template)
149
+ if template.respond_to?(:path)
150
+ path =~ /^admin/ ? false : true
151
+ else
152
+ true
153
+ end
154
+ end
155
+ end
156
+
157
+ ActionView::Template.register_template_handler(:erb, MyI18nTemplateHandler.new)
158
+
159
+ ## Testing
160
+
161
+ ### Setup
162
+
163
+ $ rvm alias create rails23_r187 ruby-1.8.7
164
+ $ rvm alias create rails30_r193 ruby-1.9.3
165
+ $ rvm alias create rails31_r193 ruby-1.9.3
166
+
167
+ $ gem install multiversion
168
+ $ multiversion all bundle install
169
+
170
+ ### Run
171
+
172
+ Against all versions:
173
+
174
+ $ multiversion all exec testrb test/*_test.rb
175
+
176
+ Against specific versions:
177
+
178
+ $ multiversion rails30_r193,rails31_r193 exec testrb test/*_test.rb
179
+
180
+
181
+ ## Extract phrases
182
+
183
+ $ i18n_template --help
184
+ extract_phrases - extract phrases for translations
185
+ --format plain|gettext|yaml translation format (default gettext)
186
+ --po-root PO ROOT root directly for po files (default po)
187
+ --glob GLOB template files glob (default app/views/**/*.{erb,rhtml})
188
+ --textdomain TEXTDOMAIN gettext textdomain (default phrases)
189
+ --output-file FILE output file (default template_phrases.txt)
190
+ --locales-root DIRECTORY locales directory (default config/locales)
191
+
192
+ ### Plain format
193
+
194
+ $ i18n_template extract_phrases --format plain --output-file /tmp/phrases.txt
195
+
196
+ ### Yaml format
197
+
198
+ $ i18n_template extract_phrases --format yaml
199
+
200
+ $ cat config/locales/phrases.yml
201
+ en:
202
+ Hello {user name}, {message}:
203
+ '[1]First name[/1] : {profile first name}':
204
+ '[1]Last name[/1] : {profile last name}':
205
+ '[1]Email[/1] : {account email}':
206
+ Copyright {current year}. All rights reserved.:
207
+
208
+ ### Gettext format
209
+
210
+ $ i18n_template extract_phrases \
211
+ --textdomain myapp \
212
+ --glob app/views/**/*.erb \
213
+ --glob lib/view/**/*.erb
214
+
215
+ $ tree --dirsfirst po
216
+ po
217
+ ├── de
218
+ │   └── myapp.po
219
+ └── myapp.pot
220
+
221
+ $ cat po/phrases.pot
222
+ # SOME DESCRIPTIVE TITLE.
223
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
224
+ # This file is distributed under the same license as the PACKAGE package.
225
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
226
+ #
227
+ msgid ""
228
+ msgstr ""
229
+ "Project-Id-Version: PACKAGE VERSION\n"
230
+ "POT-Creation-Date: 2011-11-28 15:38+0200\n"
231
+ "PO-Revision-Date: 2011-11-25 21:27+0200\n"
232
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
233
+ "Language-Team: LANGUAGE <LL@li.org>\n"
234
+ "Language: \n"
235
+ "MIME-Version: 1.0\n"
236
+ "Content-Type: text/plain; charset=UTF-8\n"
237
+ "Content-Transfer-Encoding: 8bit\n"
238
+ "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
239
+
240
+ # app/views/_footer.html.erb
241
+ msgid "Copyright {current year}. All rights reserved."
242
+ msgstr ""
243
+
244
+ # app/views/greeting.html.erb
245
+ msgid "Hello {user name}, {message}"
246
+ msgstr ""
247
+
248
+ ## References
249
+
250
+ * [multiversion](https://github.com/railsware/multiversion)
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'i18n_template'
4
+
5
+ I18nTemplate::Runner.run
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "i18n_template/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "i18n_template"
7
+ s.version = I18nTemplate::VERSION
8
+ s.authors = ["Nikolai Lugovoi", "Yaroslav Lazor", "Andriy Yanko"]
9
+ s.email = ["andriy.yanko@gmail.com"]
10
+ s.homepage = "https://github.com/railsware/i18n_template"
11
+ s.summary = %q{I18nTemplate is made to extract phrases from html/xhtml/xml documents and translate them on the fly}
12
+ s.description = %q{
13
+ I18nTemplate is made to extract phrases and translate templates.
14
+ Currently I18nTemplate can work with (x)html documents.
15
+ Translation is done by modify the original template (on the fly) to be translated on erb execution time.
16
+ }
17
+
18
+ s.rubyforge_project = "i18n_template"
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+
25
+ s.add_runtime_dependency "actionpack", ">=2.3.0"
26
+ end
@@ -0,0 +1,28 @@
1
+ require "i18n_template/version"
2
+
3
+ module I18nTemplate
4
+ autoload :Handler, 'i18n_template/handler'
5
+ autoload :Translator, 'i18n_template/translator'
6
+ autoload :Extractor, 'i18n_template/extractor'
7
+ autoload :Runner, 'i18n_template/runner'
8
+ autoload :Translation, 'i18n_template/translation'
9
+ autoload :Document, 'i18n_template/document'
10
+ autoload :Node, 'i18n_template/node'
11
+
12
+ class << self
13
+ def runners
14
+ @runners ||= []
15
+ end
16
+
17
+ def extractors
18
+ @extractors ||= []
19
+ end
20
+
21
+ def translator
22
+ @translator ||= I18nTemplate::Translator::I18n
23
+ end
24
+ attr_writer :translator
25
+ end
26
+ end
27
+
28
+ require "i18n_template/railtie"
@@ -0,0 +1,460 @@
1
+ # encoding: UTF-8
2
+ require 'active_support'
3
+ require 'active_support/core_ext/string'
4
+ require 'action_controller/vendor/html-scanner/html/tokenizer'
5
+
6
+ module I18nTemplate
7
+ ##
8
+ # I18nTemplate::Document processes on the fly xhtml document internationalization.
9
+ #
10
+ # @example
11
+ #
12
+ # Next document will be automatically internationalized
13
+ #
14
+ # <body>
15
+ # <% current_year = Time.now.year %>
16
+ # <span i18n="p">hello</span>
17
+ # <h2>Dashboard</h2>
18
+ # <div>Posts count: <%= current_user.posts.count %></div>
19
+ # <div>Click<a href="#">here</a></div>
20
+ # </body>
21
+ #
22
+ # to:
23
+ #
24
+ # <body>
25
+ # <% current_year = Time.now.year %>
26
+ # <span>
27
+ # <%- i18n_variables = {}; i18n_wrappers = [] -%>
28
+ # <%= ::I18nTemplate::Translation.translate("hello", i18n_wrappers, i18n_variables) %>
29
+ # </span>
30
+ # <h2>
31
+ # <%- i18n_variables = {}; i18n_wrappers = [] -%>
32
+ # <%= ::I18nTemplate::Translation.translate("Dashboard", i18n_wrappers, i18n_variables) %>
33
+ # </h2>
34
+ # <div>
35
+ # <%- i18n_variables = {}; i18n_wrappers = [] -%>
36
+ # <%- i18n_variables['current user posts count'] = capture do -%>
37
+ # <%= current_user.posts.count %>
38
+ # <%- end -%>
39
+ # <%= ::I18nTemplate::Translation.translate("Posts count: {current user posts count}",
40
+ # i18n_wrappers, i18n_variables) %>
41
+ # </div>
42
+ # <div>
43
+ # <%- i18n_variables = {}; i18n_wrappers = [] -%>
44
+ # <%- i18n_wrappers[1] = capture do -%>
45
+ # <a href="#" i18n_wrapper="1">
46
+ # <%- i18n_variables = {}; i18n_wrappers = [] -%>
47
+ # <%= ::I18nTemplate::Translation.translate("here", i18n_wrappers, i18n_variables) %>
48
+ # </a>
49
+ # <%- end -%>
50
+ # <%= ::I18nTemplate::Translation.translate("Click[1]here[/1]", i18n_wrappers, i18n_variables) %>
51
+ # </div>
52
+ # </body>
53
+ #
54
+ # So you need just tp translate next phrases:
55
+ #
56
+ # * _hello_
57
+ # * _Dashboard_
58
+ # * _Posts count: {current user posts count}_
59
+ # * _Click[1]here[/1]_
60
+ #
61
+ # I18n special markup element/attributes:
62
+ #
63
+ # * <i18n>content</i18n> - mark invisible for parser content for internationalization
64
+ # * <... i18n="i" ...>content<...> - (ignore) ignore element content internationalization
65
+ # * <... i18n="p" ...>content<...> - (phrase) explicitly enable content internationalization
66
+ # * <... i18n="s" ...>content<...> - (subphrase) mark element content as subphrase for parent element phrase
67
+ #
68
+ # Internal i18n element/attributes/scriptlets:
69
+ #
70
+ # * < ... i18n_phrase="phrase content" ...> - set extracted phrase into attribute
71
+ # * < ... i18n_wrapper="position" ...> - mark element as wrapper as position in i18n_wrappers array
72
+ # * <i18n_variable name="variable name">variable value</i18n_variable> - holds captured variable value with specified variable name from i18n_variables hash
73
+ # * <% i18n_wrappers %> - array of captured wrapper contents
74
+ # * <% i18n_variables %> - hash of name-value where name is variable name and value is captured variable value
75
+
76
+ class Document
77
+ # a symbol that means fold start
78
+ FOLD_START = [0x2264].pack("U*").freeze
79
+
80
+ # a symbol that means fold end
81
+ FOLD_END = [0x2265].pack("U*").freeze
82
+
83
+ # folds mapping
84
+ FOLDS = [
85
+ [ 'ignore', /<!DOCTYPE--.+?-->/m ],
86
+ [ 'ignore', /<script[^>]*?>.+?<\/script>/m ],
87
+ [ 'ignore', /<!--.+?-->/m ],
88
+ [ 'ignore', /<style[^>]*?>.+?<\/style>/m ],
89
+ [ 'eval', /<select.+?<\/select>/m ],
90
+ [ 'ignore', /<%[^=](.*?)%>/m ],
91
+ [ 'eval', /<%=(.*?)%>/m ]
92
+ ].freeze
93
+
94
+ # $1 - fold index
95
+ # $2 - fold type e.g (eval, ignore)
96
+ FOLD = /#{FOLD_START}(\d+):(\w+)#{FOLD_END}/.freeze
97
+
98
+ # $1 tag name. E.g a-b:c_d
99
+ OPEN_TAG = /^<(\w+(:[\w_-]+)?)/.freeze
100
+
101
+ # $1 tag name. E.g a-b:c_d
102
+ CLOSED_TAG = /<\/(\w+(:[\w_-]+)?)>/.freeze
103
+
104
+ SELF_CLOSE = /\/>$/.freeze
105
+
106
+ BLOCK_TAGS = %w(
107
+ i18n address blockquote p div h1 h2 h3 h4 h5 h6 li dd dt td th a
108
+ legend label title caption option optgroup button
109
+ ).freeze
110
+
111
+ # &#169; &copy;
112
+ HTML_ENTITY = /&(#\d+|\w+);/
113
+
114
+ # processed document source
115
+ attr_reader :source
116
+
117
+ # array of processing warings
118
+ attr_reader :warnings
119
+
120
+ # array of folds
121
+ attr_reader :folds
122
+
123
+ # array of translation phrases
124
+ attr_reader :phrases
125
+
126
+ # root document node
127
+ attr_reader :root_node
128
+
129
+ # stack of document nodes
130
+ attr_reader :node_stack
131
+
132
+ # Initialize document processor
133
+ # @param [String] document a pure html/xml document or erb template
134
+ def initialize(source)
135
+ @source = source.dup
136
+ @warnings = []
137
+ @folds = []
138
+ @phrases = []
139
+ end
140
+
141
+ # Pre process document:
142
+ # * add translation key attributes
143
+ # * extract translation phrases
144
+ # * modify document source
145
+ # @return true
146
+ def preprocess!
147
+ raise "Document is already preprocessed" if @preprocessed
148
+
149
+ fold_special_tags!
150
+
151
+ parse_nodes do |node|
152
+ set_node_phrase(node)
153
+ end
154
+
155
+ @source = ""
156
+ @node_stack.each do |node|
157
+ @source << node_to_text(node)
158
+ end
159
+
160
+ @preprocessed = true
161
+ end
162
+
163
+ # Processs a document:
164
+ # * expand translation keys
165
+ # * modify document source
166
+ def process!
167
+ raise "Document is already processed" if @processed
168
+
169
+ preprocess!
170
+ parse_nodes
171
+
172
+ @source = ""
173
+ @root_node.children.each { |node| translate_node(node) }
174
+ unfold_special_tags!
175
+
176
+ @processed = true
177
+ end
178
+
179
+ # return true if document is preprocessed?
180
+ def preprocessed?
181
+ @preprocessed
182
+ end
183
+
184
+ # return true if document is processed?
185
+ def processed?
186
+ @processed
187
+ end
188
+
189
+ protected
190
+
191
+ # convert special tags to string FOLD_STARTindex:nameFOLD_END
192
+ # push tag and content to folds array
193
+ def fold_special_tags!
194
+ @folds = []
195
+
196
+ FOLDS.each do |name, pattern|
197
+ @source.gsub!(pattern) do |content|
198
+ fold = "#{FOLD_START}#{@folds.size}:#{name}#{FOLD_END}"
199
+ @folds << content
200
+ fold
201
+ end
202
+ end
203
+ end
204
+
205
+ # replace FOLD_STARTindex:nameFOLD_END with @folds[index]
206
+ def unfold_special_tags!
207
+ @source.gsub!(FOLD) { @folds[$1.to_i] }
208
+ end
209
+
210
+ def parse_nodes
211
+ @root_node = Node.new(nil, 0, 0, "ROOT", "ROOT") { @parent = self }
212
+ @node_stack = []
213
+ current_node = @root_node
214
+
215
+ tokenizer = ::HTML::Tokenizer.new(@source)
216
+
217
+ while token = tokenizer.next
218
+ case token
219
+ when OPEN_TAG
220
+ node = Node.new(current_node, tokenizer.line, tokenizer.position, token, $1)
221
+ @node_stack.push node
222
+ current_node.children.push node
223
+ current_node = node unless token =~ SELF_CLOSE
224
+ when CLOSED_TAG
225
+ node = Node.new(current_node, tokenizer.line, tokenizer.position, token, $1)
226
+ warn("EXTRA CLOSING TAG:#{node.tag}, UP:#{current_node.token}", node.line) unless current_node.token[1, node.tag.size] == node.tag
227
+ @node_stack.push node
228
+ current_node = current_node.parent
229
+ else
230
+ node = Node.new(current_node, tokenizer.line, tokenizer.position, token)
231
+ @node_stack.push node
232
+ current_node.children.push node
233
+ end
234
+
235
+ yield @node_stack.last if block_given?
236
+ end
237
+ end
238
+
239
+ # Escape next characters:
240
+ # * '[' - [lsb] left square bracket
241
+ # * ']' - [rsb] right square bracket
242
+ # * '{' - [lcb] left curly bracket
243
+ # * '}' - [rcb] right curly bracket
244
+ # * '#' - [ns] number sign
245
+ def escape_phrase(phrase)
246
+ phrase.gsub(/(\[|\]|\{|\}|#)/) do |char|
247
+ case char
248
+ when '[' then '[lsb]'
249
+ when ']' then '[rsb]'
250
+ when '{' then '[lcb]'
251
+ when '}' then '[rcb]'
252
+ when '#' then '[ns]'
253
+ else
254
+ char
255
+ end
256
+ end
257
+ end
258
+
259
+ def set_node_phrase(node)
260
+ return if node.tag?
261
+ return if node.token.blank?
262
+ return if node.token.split(/\s+/).all? { |v| v =~ HTML_ENTITY }
263
+
264
+ phrase = node.token.dup
265
+ phrase.gsub!(/"/, '&quot;')
266
+ phrase.gsub!(/\r\n/, ' ')
267
+ phrase.gsub!(/\s+/, ' ')
268
+ phrase.strip!
269
+
270
+
271
+ until node.parent.root?
272
+ break if node.phrase ||
273
+ node.token =~ /i18n="(p|i)"/ ||
274
+ (BLOCK_TAGS.include?(node.tag) && node.token !~ /i18n="s"/)
275
+ node = node.parent
276
+ end
277
+ return if node.token =~ /i18n="i"/
278
+
279
+ node.phrase ||= ''
280
+ node.phrase << " " << phrase
281
+ end
282
+
283
+ def node_to_text(node)
284
+ node_text = node.token.dup
285
+ node_text.gsub!(FOLD) { @folds[$1.to_i] }
286
+
287
+ return node_text if node.phrase.nil? || node.phrase.strip.split(/ /).all? { |value| value =~ FOLD }
288
+
289
+ # push down phrase for cases like <div><span><span>Text</span></span></div>
290
+ if node.children.first && node.children.first.tag? &&
291
+ (node.children.size == 1 || node.wrapped_node_text)
292
+ node.children.first.phrase = node.phrase
293
+ node.phrase = nil
294
+ return node_text
295
+ end
296
+
297
+ # allowed fold indices
298
+ fold_indices = []
299
+ node.phrase.scan(FOLD).each do |index, type|
300
+ next unless type == 'eval'
301
+ fold_indices.push(index.to_i)
302
+ end
303
+
304
+ phrase = ""
305
+ wrap_counter = 0
306
+ node.children.each do |child|
307
+ if child.text?
308
+ #phrase << unfold_text(child.token, fold_indices)
309
+ text = escape_phrase(child.token)
310
+ phrase << text
311
+ elsif child.tag == 'br'
312
+ phrase << "[nl]"
313
+ else
314
+ wrap_counter += 1
315
+ child.token.sub!(/>$/, " i18n_wrapper=\"#{wrap_counter}\">")
316
+ if text = child.wrapped_node_text
317
+ #text = unfold_text(text, fold_indices)
318
+ phrase << "[#{wrap_counter}]#{text}[/#{wrap_counter}]"
319
+ else
320
+ text = child.descendants_text
321
+ #text = unfold_text(node.descendants_text, fold_indices)
322
+ phrase << "NNODE[#{wrap_counter}]#{text}[/#{wrap_counter}]"
323
+ end
324
+ end
325
+ end
326
+
327
+ # unfold phrase
328
+ unfold_text!(phrase, fold_indices)
329
+
330
+ # wrap variables in text nodes
331
+ wrap_variables(node, fold_indices)
332
+
333
+ phrase.gsub!(/\s+/, ' ')
334
+ phrase.gsub!(/"/, '&quot;')
335
+ phrase.strip!
336
+
337
+ # append translation key attribute
338
+ unless phrase.blank?
339
+ @phrases << phrase
340
+ node_text.sub!(/>$/) { " i18n_phrase=\"#{phrase}\">" }
341
+ end
342
+
343
+ node_text
344
+ end
345
+
346
+ def unfold_text!(text, fold_indices)
347
+ text.gsub!(FOLD) do |string|
348
+ index = $1.to_i
349
+
350
+ if $2 == 'eval' && fold_indices.include?(index)
351
+ '{' << fold_human_variable(index) << '}'
352
+ else
353
+ string
354
+ end
355
+ end
356
+ end
357
+
358
+ def wrap_variables(node, fold_indices)
359
+ node.children.each do |child|
360
+
361
+ child.token.gsub!(FOLD) do |string|
362
+ index = $1.to_i
363
+ if $2 == 'eval' && fold_indices.include?(index)
364
+ "<i18n_variable name=\"" << fold_human_variable(index) << "\">#{string}</i18n_variable>"
365
+ else
366
+ string
367
+ end
368
+ end if child.text?
369
+
370
+ wrap_variables(child, fold_indices)
371
+ end
372
+ end
373
+
374
+ def fold_human_variable(index)
375
+ fold = @folds[index]
376
+
377
+ var = fold.dup
378
+ var.sub!(/^<%=/, '')
379
+ var.gsub!(/<\/?[^>]+>/, '')
380
+ var.gsub!(/\W+/, ' ')
381
+ var.gsub!(/_/, ' ')
382
+ var.strip!
383
+ parts = var.split(/\s+/)
384
+
385
+ parts.shift if parts[0] == 'h' || parts[0] == 'render' || parts[0] == 'f'
386
+ parts.shift if parts[0] == 'partial'
387
+
388
+ 3.times { parts.shift } if parts[0,3] == ['check', 'box', 'tag']
389
+ 3.times { parts.shift } if parts[0,3] == ['radio', 'button', 'tag']
390
+ 3.times { parts.shift } if parts[0,3] == ['text', 'field', 'tag']
391
+ 2.times { parts.shift } if parts[0,2] == ['select', 'tag']
392
+
393
+ variable = (parts.size > 3 ? parts[0,5] : parts).join(" ")
394
+ warn "EMPTY VARIABLE:#{fold}" if variable.empty?
395
+ variable
396
+ end
397
+
398
+ def translate_node(node, translate = true, notext = false)
399
+ if node.text?
400
+ @source << node.token unless notext
401
+ else
402
+ if node.token =~ /i18n_phrase/
403
+ warn("NESTED T9N:#{node.tag} UP #{node.parent.token}", node.line) unless translate
404
+ node.token.sub!(/ i18n_phrase="(.+?)"/, '')
405
+ key = $1.dup
406
+ node.token.sub!(/ i18n="p"/, '')
407
+
408
+ warn("BLOCK NOT EXPANDED:#{node.token} #{key}", node.line) if key =~ FOLD
409
+ warn("NODE MISSING:#{node.token} #{key}", node.line) if key =~ /NNODE/
410
+
411
+ @source << node.token unless node.tag == 'i18n'
412
+
413
+ @source << "<%- i18n_variables = {}; i18n_wrappers = [] -%>"
414
+ node.children.each do |child|
415
+ if child.tag?
416
+ if child.token =~ /<i18n_variable name="(.*?)"/
417
+ @source << "<%- i18n_variables['#$1'] = capture do -%>"
418
+ translate_node(child.children.first, false, false)
419
+ @source << "<%- end -%>"
420
+ elsif child.token =~ /i18n_wrapper=\"(\d+)\"/
421
+ @source << "<%- i18n_wrappers[#$1] = capture do -%>"
422
+ translate_node(child, false, true)
423
+ @source << "<%- end -%>"
424
+ end
425
+ end
426
+ end
427
+
428
+ @source << "<%= ::I18nTemplate::Translation.translate(#{key.inspect}, i18n_wrappers, i18n_variables) %>"
429
+ @source << "</#{node.tag}>" unless node.tag == 'i18n' || node.tag[-2,2] == '/>'
430
+
431
+ return
432
+ elsif node.token =~ /<i18n_variable name="(.*?)"/
433
+ @source << "<%- i18n_variables['#$1'] = capture do -%>"
434
+ translate_node(node.children.first, false, false)
435
+ @source << "<%- end -%>"
436
+
437
+ return
438
+ end
439
+
440
+ @source << node.token.sub(/\s+i18n="i"/, '') unless node.tag == 'i18n'
441
+ node.children.each do |child|
442
+ translate_node(child, translate, notext)
443
+ end
444
+ @source << "</#{node.tag}>" unless node.tag == 'i18n' || node.tag[-2,2] == '/>'
445
+ end
446
+ end
447
+
448
+ # Record waring
449
+ # @param [String] message a message
450
+ # @param [String] line (optional) a line in source
451
+ def warn(message, line = nil)
452
+ if line
453
+ @warnings << "[SOURCE:#{line}]: #{message}"
454
+ else
455
+ @warnings << message
456
+ end
457
+ end
458
+
459
+ end
460
+ end