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