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,132 @@
1
+ require 'i18n'
2
+ require 'stringio'
3
+ require 'etest'
4
+
5
+ module Localite
6
+ module Backend; end
7
+ end
8
+
9
+ require "#{File.dirname(__FILE__)}/tr"
10
+
11
+ class Localite::Backend::Simple < I18n::Backend::Simple
12
+ def initialize(*args)
13
+ @locales = args.map(&:to_s) unless args.empty?
14
+ end
15
+
16
+ def available_locales
17
+ if @locales
18
+ @locales.map(&:to_sym)
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ #
25
+ # Loads a single translations file by delegating to #load_rb or
26
+ # #load_yml depending on the file extension and directly merges the
27
+ # data to the existing translations. Raises I18n::UnknownFileType
28
+ # for all other file extensions.
29
+ def load_file(filename)
30
+ locale_from_file = File.basename(filename).sub(/\.[^\.]+$/, "")
31
+ if @locales && !@locales.include?(locale_from_file)
32
+ # dlog "Skipping translations from #{filename}"
33
+ return
34
+ end
35
+
36
+ Localite.logger.warn "Loading translations from #{filename}"
37
+ type = File.extname(filename).tr('.', '').downcase
38
+ raise I18n::Backend::Simple::UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
39
+
40
+ data = send :"load_#{type}", filename
41
+
42
+ #
43
+ #
44
+ if locale_from_file.length == 2 && data.keys.map(&:to_s) != [ locale_from_file ]
45
+ merge_translations(locale_from_file, data)
46
+ else
47
+ data.each { |locale, d| merge_translations(locale, d) }
48
+ end
49
+ end
50
+
51
+ #
52
+ # add an additional translation storage layer.
53
+ def merge_translations(locale, data)
54
+ translations_for_locale!(locale).update data
55
+ super
56
+ rescue IndexError
57
+ end
58
+
59
+ def translations
60
+ @translations ||= {}
61
+ end
62
+
63
+ def keys_for_locale(locale)
64
+ (translations[locale.to_sym] || {}).keys.sort
65
+ end
66
+
67
+ def keys
68
+ r = []
69
+ translations.each do |k,v|
70
+ r += v.keys
71
+ end
72
+ r.sort.uniq
73
+ end
74
+
75
+ def translations_for_locale(locale)
76
+ translations[locale.to_sym]
77
+ end
78
+
79
+ def translations_for_locale!(locale)
80
+ translations[locale.to_sym] ||= {}
81
+ end
82
+
83
+ #
84
+ # monkeypatches "I18n::Backend::Simple"
85
+ #
86
+ # Looks up a translation from the translations hash. Returns nil if
87
+ # eiher key is nil, or locale, scope or key do not exist as a key in the
88
+ # nested translations hash. Splits keys or scopes containing dots
89
+ # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
90
+ # <tt>%w(currency format)</tt>.
91
+ #
92
+ # We are doing this because the default simple storage does not support
93
+ # keys, that inself are part of other keys, i.e. "field" and "field.subfield"
94
+ def lookup(locale, key, scope = [], options = {})
95
+ return unless key
96
+ init_translations unless initialized?
97
+
98
+ keys = I18n.normalize_keys(locale, key, scope, options[:separator])
99
+
100
+ #
101
+ # the first key is the locale; all other keys make up the final key.
102
+ lookup_localite_storage(*keys) || begin
103
+ keys.inject(translations) do |result, key|
104
+ key = key.to_sym
105
+ return nil unless result.is_a?(Hash) && result.has_key?(key)
106
+ result = result[key]
107
+ result = resolve(locale, key, result, options) if result.is_a?(Symbol)
108
+ String === result ? result.dup : result
109
+ end
110
+ end
111
+ end
112
+
113
+ def lookup_localite_storage(locale, *keys)
114
+ return nil unless tr = translations_for_locale(locale)
115
+ r = tr[keys.join(".")]
116
+ end
117
+
118
+ #
119
+ # The main differences to a yaml file are: ".tr" files support
120
+ # specific and lower level entries for the same key, and allows
121
+ # to reopen a key.
122
+ #
123
+ # x:
124
+ # y: "x.y translation"
125
+ # z: "x.y.z translation"
126
+ # y:
127
+ # a: "x.y.a translation"
128
+ #
129
+ def load_tr(filename)
130
+ Localite::Backend::Tr.load(filename)
131
+ end
132
+ end
@@ -1,8 +1,16 @@
1
1
  require "cgi"
2
2
 
3
3
  class Localite::Template < String
4
- def self.run(mode, template, opts = {})
5
- new(template).run(mode, opts)
4
+ def self.run(template, opts = {})
5
+ new(template).run(Localite.current_format, opts)
6
+ end
7
+
8
+ def self.html(template, opts = {})
9
+ new(template).run(:html, opts)
10
+ end
11
+
12
+ def self.text(template, opts = {})
13
+ new(template).run(:text, opts)
6
14
  end
7
15
 
8
16
  #
@@ -65,7 +73,8 @@ class Localite::Template < String
65
73
  end
66
74
  end
67
75
 
68
- def run(mode, opts = {})
76
+ def run(format, opts = {})
77
+
69
78
  #
70
79
  env = Env.new(opts)
71
80
 
@@ -73,28 +82,23 @@ class Localite::Template < String
73
82
  # get all --> {* code *} <-- parts from the template strings and send
74
83
  # them thru the environment.
75
84
  gsub(/\{\*([^\}]+?)\*\}/) do |_|
76
- Modi.send mode, env[$1]
85
+ Localite::Format.send format, env[$1]
77
86
  end
78
87
  end
79
-
80
- module Modi
81
- def self.text(s); s; end
82
- def self.html(s); CGI.escapeHTML(s); end
83
- end
84
88
  end
85
89
 
86
90
  module Localite::Template::Etest
87
91
  Template = Localite::Template
88
92
 
89
93
  def test_templates
90
- assert_equal "abc", Template.run(:text, "{*xyz*}", :xyz => "abc")
91
- assert_equal "3 items", Template.run(:text, "{*pl 'item', xyz.length*}", :xyz => "abc")
92
- assert_equal "xyz", Template.run(:text, "xyz", :xyz => "abc")
93
- assert_equal "abc", Template.run(:text, "{*xyz*}", :xyz => "abc")
94
- assert_equal "3", Template.run(:text, "{*xyz.length*}", :xyz => "abc")
95
- assert_equal "3", Template.run(:text, "{*xyz.length*}", :xyz => "abc")
96
- assert_equal "3 Fixnums", Template.run(:text, "{*pl xyz*}", :xyz => [1, 2, 3])
97
- assert_equal "3 Fixnums and 1 Float", Template.run(:text, "{*pl xyz*} and {*pl fl*}", :xyz => [1, 2, 3], :fl => [1.0])
94
+ assert_equal "abc", Template.text("{*xyz*}", :xyz => "abc")
95
+ assert_equal "3 items", Template.text("{*pl 'item', xyz.length*}", :xyz => "abc")
96
+ assert_equal "xyz", Template.text("xyz", :xyz => "abc")
97
+ assert_equal "abc", Template.text("{*xyz*}", :xyz => "abc")
98
+ assert_equal "3", Template.text("{*xyz.length*}", :xyz => "abc")
99
+ assert_equal "3", Template.text("{*xyz.length*}", :xyz => "abc")
100
+ assert_equal "3 Fixnums", Template.text("{*pl xyz*}", :xyz => [1, 2, 3])
101
+ assert_equal "3 Fixnums and 1 Float", Template.text("{*pl xyz*} and {*pl fl*}", :xyz => [1, 2, 3], :fl => [1.0])
98
102
  end
99
103
 
100
104
  class Name < String
@@ -104,7 +108,7 @@ module Localite::Template::Etest
104
108
  end
105
109
 
106
110
  def test_nohash
107
- assert_equal "abc", Template.run(:text, "{*name*}", Name.new("abc"))
111
+ assert_equal "abc", Template.text("{*name*}", Name.new("abc"))
108
112
  end
109
113
 
110
114
  def test_pl
@@ -121,20 +125,20 @@ module Localite::Template::Etest
121
125
  end
122
126
 
123
127
  def test_html_env
124
- assert_equal "a>c", Template.run(:text, "{*xyz*}", :xyz => "a>c")
125
- assert_equal "a&gt;c", Template.run(:html, "{*xyz*}", :xyz => "a>c")
126
- assert_equal "> a>c", Template.run(:text, "> {*xyz*}", :xyz => "a>c")
127
- assert_equal "> a&gt;c", Template.run(:html, "> {*xyz*}", :xyz => "a>c")
128
+ assert_equal "a>c", Template.text("{*xyz*}", :xyz => "a>c")
129
+ assert_equal "a&gt;c", Template.html("{*xyz*}", :xyz => "a>c")
130
+ assert_equal "> a>c", Template.text("> {*xyz*}", :xyz => "a>c")
131
+ assert_equal "> a&gt;c", Template.html("> {*xyz*}", :xyz => "a>c")
128
132
  end
129
133
 
130
134
  def test_template_hash
131
- assert_equal "a>c", Template.run(:text, "{*xyz*}", :xyz => "a>c")
132
- assert_equal "a>c", Template.run(:text, "{*xyz*}", "xyz" => "a>c")
135
+ assert_equal "a>c", Template.text("{*xyz*}", :xyz => "a>c")
136
+ assert_equal "a>c", Template.text("{*xyz*}", "xyz" => "a>c")
133
137
  end
134
138
 
135
139
  def test_template_hash_missing
136
140
  assert_raise(NameError) {
137
- Template.run(:text, "{*abc*}", :xyz => "a>c")
141
+ Template.text("{*abc*}", :xyz => "a>c")
138
142
  }
139
143
  end
140
144
  end
@@ -0,0 +1,292 @@
1
+ class Localite::Backend::Tr
2
+ #
3
+ # This class handles line reading from ios (files, etc.) and strings.
4
+ # It offers an one-line unread buffer.
5
+ class IO
6
+ attr_reader :lineno
7
+
8
+ def initialize(src)
9
+ @lineno = 0
10
+ @io = if src.is_a?(String)
11
+ StringIO.new(src)
12
+ else
13
+ src
14
+ end
15
+ end
16
+
17
+ def eof?
18
+ @unreadline.nil? && @io.eof?
19
+ end
20
+
21
+ def unreadline(line)
22
+ @unreadline = line
23
+ end
24
+
25
+ def readline
26
+ r, @unreadline = @unreadline, nil
27
+ r || begin
28
+ @lineno += 1
29
+ @io.readline
30
+ end
31
+ end
32
+ end
33
+
34
+ def self.parse(io, name=nil, &block)
35
+ new(io, name).parse(&block)
36
+ end
37
+
38
+ def self.load(file, &block)
39
+ new(File.open(file), file).parse(&block)
40
+ end
41
+
42
+ def initialize(src, name=nil)
43
+ @src, @name = src, name
44
+ end
45
+
46
+ attr_reader :indent, :name
47
+
48
+ def keys
49
+ return @keys if @keys
50
+
51
+ if @parse
52
+ @keys = @parse.keys
53
+ else
54
+ @keys = []
55
+ parse_ do |k,_|
56
+ @keys << k
57
+ end
58
+ @keys.uniq!
59
+ end
60
+
61
+ @keys.sort!
62
+ end
63
+
64
+ def parse(&block)
65
+ if block_given?
66
+ parse_(&block)
67
+ else
68
+ @parse ||= begin
69
+ hash = {}
70
+ parse_ do |k,v|
71
+ duplicate_entry(k) if hash.key?(k)
72
+ hash[k] = v
73
+ end
74
+ hash
75
+ end
76
+ end
77
+ end
78
+
79
+ protected
80
+
81
+ def duplicate_entry(k)
82
+ msg = "[#{name}] " if name
83
+ Localite.logger.warn "#{msg}Duplicate entry" + k.inspect
84
+ end
85
+
86
+ private
87
+
88
+ #
89
+ # -- TR scopes
90
+ def register_scope(indent, name)
91
+ @indent = indent
92
+ @scopes = @scopes[0, indent]
93
+ @scopes[indent] = evaluate(name)
94
+ end
95
+
96
+ def current_scope
97
+ @scopes[0..@indent].compact.join(".")
98
+ end
99
+
100
+ def parse_(&block)
101
+ @io = IO.new @src
102
+ @scopes = []
103
+ while !io.eof? do
104
+ line = io.readline
105
+
106
+ #
107
+ # skip empty and comment lines
108
+ next if line =~ /^\s*(#|$)/
109
+
110
+ if line =~ /^(\s*)([^:]+):\s*\|\s*$/ # Start of a multiline entry?
111
+ register_scope $1.length, $2
112
+ yield current_scope, read_multiline_value
113
+ elsif line =~ /^(\s*)([^:]+):\s*([^|].*)/ # A singleline entry?
114
+ register_scope $1.length, $2
115
+ value = $3.sub(/\s+$/, "")
116
+ yield current_scope, evaluate(value) unless value.empty?
117
+ else
118
+ msg = name ? "#{name}(#{io.lineno})" : "Line #{io.lineno}"
119
+
120
+ msg += ": format error in #{line.inspect}"
121
+ dlog msg
122
+ raise msg
123
+ end
124
+ end
125
+ ensure
126
+ @io = nil
127
+ end
128
+
129
+ attr_reader :io
130
+
131
+ def read_multiline_value
132
+ value = []
133
+ while !io.eof? do
134
+ line = io.readline
135
+ line =~ /^(\s*)(.*)$/
136
+
137
+ if $2.empty?
138
+ value << ""
139
+ next
140
+ end
141
+
142
+ line_indent = $1.length
143
+
144
+ #
145
+ # all multiline entries have a higher indent than the current line.
146
+ if line_indent <= indent
147
+ io.unreadline(line)
148
+ break
149
+ end
150
+ value << $2.sub(/\s+$/, "")
151
+ end
152
+
153
+ value.join("\n")
154
+ end
155
+
156
+ #
157
+ # evaluate a string
158
+ def evaluate(s)
159
+ return s unless s[0] == s[-1] && (s[0] == 34 || s[0] == 39)
160
+
161
+ s[1..-2].gsub(/\\(.)/m) do
162
+ case $1
163
+ when "n" then "\n"
164
+ when "t" then "\t"
165
+ else $1
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ module Localite::Backend::Tr::Etest
172
+ def evaluate(s)
173
+ Localite::Backend::Tr.new("").send(:evaluate, s)
174
+ end
175
+
176
+ def test_tr_file
177
+ d = Localite::Backend::Tr.load(File.dirname(__FILE__) + "/../../test/i18n/en.tr")
178
+ assert_kind_of(Hash, d)
179
+ end
180
+
181
+ def test_eval_string_prereq
182
+ assert_equal 34, '""'[0]
183
+ assert_equal 34, '""'[-1]
184
+ assert_equal 39, "''"[0]
185
+ end
186
+
187
+ def test_eval_string
188
+ assert_equal "ab\nc", evaluate("'ab\nc'")
189
+ assert_equal "ab\ncdef", evaluate('"ab\ncdef"')
190
+ assert_equal "ab\tcd\nef", evaluate('"ab\tcd\nef"')
191
+ assert_equal "abbcd", evaluate('"ab\bcd"')
192
+ assert_equal "en/outer/inner/y1", evaluate("\"en/outer/inner/y1\"")
193
+ end
194
+
195
+ def test_parse_tr
196
+ tr = <<-TR
197
+ #
198
+ # comment
199
+ param: "{* xxx *}"
200
+ base: "en_only"
201
+ t: "en.t"
202
+ #
203
+ # another comment
204
+ outer:
205
+ inner:
206
+ x1: "en/outer/inner/x1"
207
+ inner:
208
+ y1: "en/outer/inner/y1"
209
+ title: "This is hypertext"
210
+ title2: "This is <> hypertext"
211
+
212
+ ml: |
213
+ mlmlml
214
+ ml: mlober
215
+
216
+ outer: |
217
+ A first multiline
218
+ entry
219
+ outer: Hey Ho!
220
+ outer: |
221
+ A multiline
222
+ entry
223
+ TR
224
+
225
+ p = Localite::Backend::Tr.new(tr)
226
+ p.stubs(:dlog) {}
227
+
228
+ assert_equal(%w(base ml outer outer.inner.x1 outer.inner.y1 param t title title2), p.keys)
229
+ data = p.parse
230
+
231
+ assert_equal("en/outer/inner/x1", data["outer.inner.x1"])
232
+ assert_equal(nil, data["outer.inner"])
233
+ assert_equal("A multiline\nentry", data["outer"])
234
+ assert_equal("mlober", data["ml"])
235
+ assert_kind_of(Hash, data)
236
+ end
237
+
238
+ def test_parse_tr_invalid
239
+ tr = <<-TR
240
+
241
+ ml: | av
242
+ mlmlml
243
+ ml: mlober
244
+ TR
245
+
246
+ p = Localite::Backend::Tr.new(tr)
247
+ p.stubs(:dlog) {}
248
+ assert_raise(RuntimeError) {
249
+ p.parse
250
+ }
251
+ end
252
+
253
+ def test_parse_from_io
254
+ tr = " ml: mlober"
255
+ d = Localite::Backend::Tr.parse(tr)
256
+ assert_equal({"ml" => "mlober"}, d)
257
+ end
258
+
259
+ def test_parse_key_names
260
+ tr = <<TR
261
+ a: aa
262
+ "b.c": abc
263
+ "b\\nc": anlc
264
+ b.c: dot
265
+ TR
266
+
267
+ d = Localite::Backend::Tr.parse(tr)
268
+ assert_equal({"a"=>"aa", "a.b.c"=>"dot", "a.b\nc"=>"anlc"}, d)
269
+ end
270
+
271
+ def test_multiline_w_spaces
272
+ tr = <<-TR
273
+ refresh:
274
+ title: t1
275
+ info: |
276
+ line1
277
+ line2
278
+
279
+ line3
280
+ line4
281
+ title: t2
282
+ TR
283
+
284
+ p = Localite::Backend::Tr.new(tr)
285
+ d = p.parse
286
+ p.stubs(:dlog) {}
287
+
288
+ assert_equal "line1\nline2\n\nline3\nline4", d["refresh.info"]
289
+ assert_equal "t2", d["refresh.title"]
290
+ end
291
+
292
+ end