localite 0.3 → 0.5.6

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 (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