localite 0.3 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/Manifest +44 -0
  2. data/VERSION +1 -1
  3. data/bin/list-tr +10 -0
  4. data/lib/localite.rb +161 -43
  5. data/lib/localite/filter.rb +23 -64
  6. data/lib/localite/format.rb +36 -0
  7. data/lib/localite/node_filter.rb +197 -0
  8. data/lib/localite/scopes.rb +93 -0
  9. data/lib/localite/settings.rb +139 -17
  10. data/lib/localite/storage.rb +132 -0
  11. data/lib/localite/template.rb +29 -25
  12. data/lib/localite/tr.rb +292 -0
  13. data/lib/localite/translate.rb +46 -20
  14. data/localite.gemspec +9 -6
  15. data/localite.tmproj +3 -99
  16. data/tasks/echoe.rake +1 -1
  17. data/test/i18n/de.tr +5 -0
  18. data/test/i18n/en.tr +16 -0
  19. data/test/rails/Rakefile +10 -0
  20. data/test/rails/app/controllers/application_controller.rb +10 -0
  21. data/test/rails/app/controllers/localite_controller.rb +20 -0
  22. data/test/rails/app/views/localite/index.html.erb +19 -0
  23. data/test/rails/app/views/localite/template.de.html.erb +1 -0
  24. data/test/rails/app/views/localite/template.en.html.erb +1 -0
  25. data/test/rails/config/boot.rb +110 -0
  26. data/test/rails/config/environment.rb +50 -0
  27. data/test/rails/config/environments/development.rb +17 -0
  28. data/test/rails/config/environments/production.rb +28 -0
  29. data/test/rails/config/environments/test.rb +28 -0
  30. data/test/rails/config/initializers/new_rails_defaults.rb +21 -0
  31. data/test/rails/config/initializers/session_store.rb +15 -0
  32. data/test/rails/config/locales/de.yml +4 -0
  33. data/test/rails/config/locales/en.yml +4 -0
  34. data/test/rails/config/routes.rb +13 -0
  35. data/test/rails/script/console +3 -0
  36. data/test/rails/script/server +3 -0
  37. data/test/rails/test/functional/localite_controller_test.rb +56 -0
  38. data/test/rails/test/test_helper.rb +38 -0
  39. data/test/test.rb +4 -1
  40. metadata +51 -11
  41. data/lib/localite/scope.rb +0 -140
  42. data/test/i18n/de.yml +0 -15
@@ -0,0 +1,36 @@
1
+ module Localite::Format
2
+ #
3
+ # convert from text into target format
4
+ def self.text(s)
5
+ case s
6
+ when /html:(.*)/m then $1
7
+ when /text:(.*)/m then $1
8
+ else s
9
+ end
10
+ end
11
+
12
+ def self.html(s)
13
+ case s
14
+ when /html:(.*)/m then $1
15
+ when /text:(.*)/m then CGI.escapeHTML($1)
16
+ else CGI.escapeHTML(s)
17
+ end
18
+ end
19
+ end
20
+
21
+ module Localite::Format::Etest
22
+ def test_format
23
+ assert_equal "xxx", Localite::Format.html("xxx")
24
+ assert_equal "xxx", Localite::Format.html("html:xxx")
25
+ assert_equal "xxx", Localite::Format.html("text:xxx")
26
+
27
+ assert_equal "&lt;&gt;", Localite::Format.html("text:<>")
28
+ assert_equal "&lt;&gt;", Localite::Format.html("<>")
29
+ assert_equal "<>", Localite::Format.html("html:<>")
30
+
31
+ assert_equal "xxx\nyyy", Localite::Format.html("xxx\nyyy")
32
+ assert_equal "xxx\nyyy", Localite::Format.html("html:xxx\nyyy")
33
+ assert_equal "xxx\nyyy", Localite::Format.html("text:xxx\nyyy")
34
+ end
35
+ end
36
+
@@ -0,0 +1,197 @@
1
+ require "nokogiri"
2
+
3
+ module Localite::NodeFilter
4
+ #
5
+ # set up filter for this action.
6
+ def self.filter(controller, &block)
7
+ yield
8
+
9
+ return unless controller.response.headers["Content-Type"] =~ /text\/html/
10
+ r = controller.response
11
+ r.body = filter_body r.body, Localite.current_locale
12
+ rescue
13
+ if Rails.env.development?
14
+ controller.response.body = "Caught exception: " + CGI::escapeHTML($!.inspect)
15
+ end
16
+ raise
17
+ end
18
+
19
+ def self.included(klass)
20
+ klass.send :around_filter, self
21
+ end
22
+
23
+ private
24
+
25
+ def self.filter_body(body, locale)
26
+ if body =~ /(<body[^>]*>)(.*)(<\/body>)/m
27
+ $`.html_safe +
28
+ $1.html_safe +
29
+ filter_node($2, locale).html_safe +
30
+ $3.html_safe +
31
+ $'.html_safe
32
+ else
33
+ filter_node(body, locale).html_safe
34
+ end
35
+ end
36
+
37
+ def self.filter_node(body, locale)
38
+ locale = locale.to_s
39
+ body = fb_mark(body)
40
+
41
+ doc = Nokogiri.HTML "<html><body>#{body}</body></html>"
42
+ doc.css("[lang]").each do |node|
43
+ next unless locale != node["lang"]
44
+ node.remove
45
+ end
46
+
47
+ doc = doc.css("body").inner_html
48
+ doc = fb_unmark(doc)
49
+ doc.html_safe
50
+ end
51
+
52
+ #
53
+ # for <fb:XXXXXX> tags to survive Nokogiri's HTML parsing we rename
54
+ # them into something non-namespacy. THIS IS A HACK! I wished I knew
55
+ # how to make
56
+ #
57
+ # a) Nokogiri.XML to let entities live, or, preferredly
58
+ # b) Nokogiri.HTML to let namespaces survive
59
+ #
60
+ FB = "fb:"
61
+ FB_RE = /(<|<\/)fb:/
62
+ FB_MARKER = "fb_marker_0xjgh_123_"
63
+ FB_MARKER_RE = Regexp.new "(<|<\/)#{FB_MARKER}"
64
+
65
+ def self.fb_mark(s)
66
+ s.gsub(FB_RE) { $1 + FB_MARKER }
67
+ end
68
+
69
+ def self.fb_unmark(s)
70
+ s.gsub(FB_MARKER_RE) { $1 + FB }
71
+ end
72
+ end
73
+
74
+ module Localite::NodeFilter::Etest
75
+ def normalize_xml(str)
76
+ str.
77
+ gsub(/>(.*?)</m) do |s| ">#{$1.gsub(/\s+/m, " ")}<" end. # normalize spaces between tags
78
+ gsub(/>\s*</m, ">\n<"). # normalize spaces in empty text nodes
79
+ gsub(/"/, "'") # normalize ' and "
80
+ end
81
+
82
+ def assert_filtered(locale, src, expected)
83
+ filtered = Localite::NodeFilter.filter_body(src, locale)
84
+ filtered = normalize_xml(filtered)
85
+
86
+ expected = normalize_xml(expected)
87
+
88
+ if expected == filtered
89
+ assert true
90
+ return
91
+ end
92
+
93
+ puts "expected:\n\n" + expected + "\n\n"
94
+ puts "filtered:\n\n" + filtered + "\n\n"
95
+
96
+ assert_equal expected, filtered
97
+ end
98
+
99
+ ##
100
+ def test_normalize_xml
101
+ assert_equal "<p>ppp</p>", normalize_xml("<p>ppp</p>")
102
+ assert_equal "<p>a b c </p>\n", normalize_xml("<p>a b c </p>\n")
103
+ end
104
+
105
+ def test_simple_html
106
+ assert_filtered :de, "<p>ppp</p>", "<p>ppp</p>"
107
+ assert_filtered :en, "<p>ppp</p>", "<p>ppp</p>"
108
+ assert_filtered :fr, "<p>ppp</p>", "<p>ppp</p>"
109
+ end
110
+
111
+ def test_simple_html_w_lang
112
+ assert_filtered :de, "<p lang='de'>ppp</p>", "<p lang='de'>ppp</p>"
113
+ assert_filtered :en, "<p lang='de'>ppp</p>", ""
114
+ assert_filtered :fr, "<p lang='de'>ppp</p>", ""
115
+
116
+ assert_filtered :de, "<p lang='en'>ppp</p>", ""
117
+ assert_filtered :en, "<p lang='en'>ppp</p>", "<p lang='en'>ppp</p>"
118
+ assert_filtered :fr, "<p lang='en'>ppp</p>", ""
119
+ end
120
+
121
+ def test_fbml_tags
122
+ assert_filtered :de, "<fb:p lang='de'>ppp</fb:p>", "<fb:p lang='de'>ppp</fb:p>"
123
+ assert_filtered :en, "<fb:p lang='de'>ppp</fb:p>", ""
124
+ assert_filtered :fr, "<fb:p lang='de'>ppp</fb:p>", ""
125
+
126
+ assert_filtered :de, "<fb:p lang='en'>ppp</fb:p>", ""
127
+ assert_filtered :en, "<fb:p lang='en'>ppp</fb:p>", "<fb:p lang='en'>ppp</fb:p>"
128
+ assert_filtered :fr, "<fb:p lang='en'>ppp</fb:p>", ""
129
+ end
130
+
131
+ def test_umlauts
132
+ assert_filtered :de, "<p lang='de'>&Auml; &Ouml;</p>", "<p lang='de'>&Auml; &Ouml;</p>"
133
+
134
+ #
135
+ # Nokogiri translates UTF-8 umlauts into entities. That is ok (for now)
136
+ assert_filtered :de, "<p lang='de'>Ä Ö</p>", "<p lang='de'>&Auml; &Ouml;</p>"
137
+ end
138
+
139
+ def test_full_html_match
140
+ src = <<-HTML
141
+ <!--Force IE6 into quirks mode with this comment tag-->
142
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
143
+ <html xmlns="http://www.w3.org/1999/xhtml">
144
+ <head>
145
+ <title>socially.io</title>
146
+ </head>
147
+ <body x="y">
148
+ <p lang='de'>&Auml; &Ouml;</p>", "<p lang='de'>&Auml; &Ouml;</p>
149
+ </body>
150
+ </html>
151
+ HTML
152
+
153
+ expected = <<-HTML
154
+ <!--Force IE6 into quirks mode with this comment tag-->
155
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
156
+ <html xmlns="http://www.w3.org/1999/xhtml">
157
+ <head>
158
+ <title>socially.io</title>
159
+ </head>
160
+ <body x="y">
161
+ <p lang='de'>&Auml; &Ouml;</p>", "<p lang='de'>&Auml; &Ouml;</p>
162
+ </body>
163
+ </html>
164
+ HTML
165
+
166
+ assert_filtered :de, src, expected
167
+ end
168
+
169
+ def test_full_html_miss
170
+ src = <<-HTML
171
+ <!--Force IE6 into quirks mode with this comment tag-->
172
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
173
+ <html xmlns="http://www.w3.org/1999/xhtml">
174
+ <head>
175
+ <title>socially.io</title>
176
+ </head>
177
+ <body x="y">
178
+ <p lang='de'>&Auml; &Ouml;</p>
179
+ </body>
180
+ </html>
181
+ HTML
182
+
183
+ expected = <<-HTML
184
+ <!--Force IE6 into quirks mode with this comment tag-->
185
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
186
+ <html xmlns="http://www.w3.org/1999/xhtml">
187
+ <head>
188
+ <title>socially.io</title>
189
+ </head>
190
+ <body x="y">
191
+ </body>
192
+ </html>
193
+ HTML
194
+
195
+ assert_filtered :en, src, expected
196
+ end
197
+ end
@@ -0,0 +1,93 @@
1
+ class Localite::Scopes < Array
2
+ def initialize
3
+ @prebuilt = []
4
+ end
5
+
6
+ def push(*array)
7
+ array.each do |s|
8
+ if @prebuilt.last
9
+ @prebuilt.push("#{@prebuilt.last}.#{s}")
10
+ else
11
+ @prebuilt.push(s.to_s)
12
+ end
13
+
14
+ super(s)
15
+ end
16
+ end
17
+
18
+ def pop(*array)
19
+ array.each do
20
+ @prebuilt.pop
21
+ super()
22
+ end
23
+ end
24
+
25
+ def each(s)
26
+ @prebuilt.reverse_each do |entry|
27
+ yield "#{entry}.#{s}"
28
+ end
29
+
30
+ yield s.to_s
31
+ end
32
+
33
+ def first(s)
34
+ if @prebuilt.last
35
+ "#{@prebuilt.last}.#{s}"
36
+ else
37
+ s
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ join(":")
43
+ end
44
+
45
+ def inspect
46
+ to_s.inspect
47
+ end
48
+ end
49
+
50
+ module Localite::Scopes::Etest
51
+ Scopes = Localite::Scopes
52
+
53
+ def test_scope
54
+ scope = Scopes.new
55
+
56
+ scope.push("a")
57
+ scope.push("b")
58
+ scope.push("b", "str")
59
+
60
+ assert_equal %w(a b b str), scope
61
+ assert_equal %w(a.b.b.str a.b.b a.b a), scope.instance_variable_get("@prebuilt").reverse
62
+ assert_equal "a:b:b:str", scope.to_s
63
+ assert_equal "\"a:b:b:str\"", scope.inspect
64
+ end
65
+
66
+ def test_more_scopes
67
+ Localite.scope("a", :b, "b") do
68
+ r = []
69
+ Localite.current_scope.each("str") do |scoped|
70
+ r << scoped
71
+ end
72
+ assert_equal %w(a.b.b.str a.b.str a.str str), r
73
+ end
74
+ end
75
+
76
+ def test_more_scopes_w_dots
77
+ Localite.scope("a", :b, "b.c.d") do
78
+ r = []
79
+ Localite.current_scope.each("str.y") do |scoped|
80
+ r << scoped
81
+ end
82
+ assert_equal %w(a.b.b.c.d.str.y a.b.str.y a.str.y str.y), r
83
+ end
84
+ end
85
+
86
+ def test_empty_scopes
87
+ r = []
88
+ Localite.current_scope.each("str.y") do |scoped|
89
+ r << scoped
90
+ end
91
+ assert_equal %w(str.y), r
92
+ end
93
+ end
@@ -1,37 +1,159 @@
1
+ #
2
+ # Localite provides three dimensions for translation keys. A call like
3
+ # that:
4
+ #
5
+ # Localite.locale(:de) do
6
+ # Localite.format(:fbml) do
7
+ # Localite.scope("outer") do
8
+ # Localite.scope("scope") do
9
+ # "msg".t
10
+ # end
11
+ # end
12
+ # end
13
+ # end
14
+ #
15
+ # looks up the following entries in these formats and languages, in
16
+ # that order:
17
+ #
18
+ # - "fbml.de.outer.scope.msg"
19
+ # - "fbml.en.outer.scope.msg"
20
+ # - "html.de.outer.scope.msg"
21
+ # - "html.en.outer.scope.msg"
22
+ # - "text.de.outer.scope.msg"
23
+ # - "text.en.outer.scope.msg"
24
+ # - "fbml.de.scope.msg"
25
+ # - "fbml.en.scope.msg"
26
+ # - "html.de.scope.msg"
27
+ # - "html.en.scope.msg"
28
+ # - "text.de.scope.msg"
29
+ # - "text.en.scope.msg"
30
+ # - "fbml.de.msg"
31
+ # - "fbml.en.msg"
32
+ # - "html.de.msg"
33
+ # - "html.en.msg"
34
+ # - "text.de.msg"
35
+ # - "text.en.msg"
36
+ #
1
37
  module Localite::Settings
2
38
  #
39
+ # == set/return the locale ==========================================
40
+
41
+ #
3
42
  # Returns the base locale; e.g. :en
4
43
  def base
5
- I18n.default_locale
44
+ :en
6
45
  end
7
46
 
8
47
  #
9
48
  # returns the current locale; defaults to the base locale
10
- def locale
11
- I18n.locale
49
+ def current_locale
50
+ Thread.current[:"localite:locale"] || base
12
51
  end
13
52
 
14
- #
15
- # is a specific locale available?
16
- def available?(locale)
17
- locale && I18n.backend.available_locales.include?(locale.to_sym)
53
+ def current_locale=(locale)
54
+ I18n.locale = Thread.current[:"localite:locale"] = locale
18
55
  end
19
56
 
20
57
  #
21
- # sets the current locale. If the locale is not available it changes
22
- # the locale to the default locale.
23
- def locale=(locale)
24
- locale = locale.to_sym
25
- I18n.locale = available?(locale) ? locale : base
58
+ # runs a block in the changed locale
59
+ def locale(locale, &block)
60
+ scope :locale => locale, &block
26
61
  end
27
62
 
28
63
  #
29
- # runs a block in the changed locale
30
- def in(locale, &block)
31
- old = self.locale
32
- self.locale = locale if locale
64
+ # scope allows to set a scope around a translation. A scoped
65
+ # translation
66
+ #
67
+ # Localite.scope("scope") do
68
+ # "msg".t
69
+ # end
70
+ #
71
+ # will look up "scope.msg" and "msg", in that order, and return the
72
+ # first matching translation in the current locale. Scopes can be
73
+ # stacked; looking up a scoped translation
74
+ #
75
+ # Localite.scope("outer") do
76
+ # Localite.scope("scope") do
77
+ # "msg".t
78
+ # end
79
+ # end
80
+ #
81
+ # will look up "outer.scope.msg", "scope.msg", "msg".
82
+ #
83
+ # If no translation will be found we look up the same entries in the base
84
+ # locale.
85
+ def scope(*args, &block)
86
+ options = if args.last.is_a?(Hash)
87
+ args.pop
88
+ else
89
+ {}
90
+ end
91
+
92
+ #
93
+ # set locale
94
+ if locale = options[:locale]
95
+ old_locale = self.current_locale
96
+ self.current_locale = if I18n.backend.available_locales.include?(locale = locale.to_sym)
97
+ locale
98
+ else
99
+ base
100
+ end
101
+ end
102
+
103
+ #
104
+ # set format
105
+ if format = options[:format]
106
+ old_format = self.current_format
107
+ self.current_format = format
108
+ end
109
+
110
+ #
111
+ # adjust scope (from the remaining arguments)
112
+ current_scope.push(*args)
113
+
33
114
  yield
34
115
  ensure
35
- self.locale = old
116
+ current_scope.pop(*args)
117
+ self.current_format = old_format if old_format
118
+ self.current_locale = old_locale if old_locale
119
+ end
120
+
121
+ def scope!(*args, &block)
122
+ old = current_scope
123
+ Thread.current[:"localite:scopes"] = Localite::Scopes.new
124
+
125
+ scope(*args, &block)
126
+ ensure
127
+ Thread.current[:"localite:scopes"] = old
128
+ end
129
+
130
+ def current_scope
131
+ Thread.current[:"localite:scopes"] ||= Localite::Scopes.new
132
+ end
133
+
134
+ #
135
+ # == format setting =================================================
136
+
137
+ #
138
+ # The format setting defines how the template engine deals with its
139
+ # parameters. In :html mode all parameters will be subject to HTML
140
+ # escaping, while in :text mode the parameters remain unchanged.
141
+ #
142
+ def format(format, &block)
143
+ scope :format => format, &block
144
+ end
145
+
146
+ def current_format
147
+ Thread.current[:"localite:format"] || :text
148
+ end
149
+
150
+ def current_format=(fmt)
151
+ Thread.current[:"localite:format"] = fmt
152
+ end
153
+
154
+ #
155
+ # == Inspect ========================================================
156
+ def inspect
157
+ "<Localite: [#{current_locale}/#{current_format}]: #{Localite.current_scope.inspect}"
36
158
  end
37
159
  end