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.
- data/Manifest +44 -0
- data/VERSION +1 -1
- data/bin/list-tr +10 -0
- data/lib/localite.rb +161 -43
- data/lib/localite/filter.rb +23 -64
- data/lib/localite/format.rb +36 -0
- data/lib/localite/node_filter.rb +197 -0
- data/lib/localite/scopes.rb +93 -0
- data/lib/localite/settings.rb +139 -17
- data/lib/localite/storage.rb +132 -0
- data/lib/localite/template.rb +29 -25
- data/lib/localite/tr.rb +292 -0
- data/lib/localite/translate.rb +46 -20
- data/localite.gemspec +9 -6
- data/localite.tmproj +3 -99
- data/tasks/echoe.rake +1 -1
- data/test/i18n/de.tr +5 -0
- data/test/i18n/en.tr +16 -0
- data/test/rails/Rakefile +10 -0
- data/test/rails/app/controllers/application_controller.rb +10 -0
- data/test/rails/app/controllers/localite_controller.rb +20 -0
- data/test/rails/app/views/localite/index.html.erb +19 -0
- data/test/rails/app/views/localite/template.de.html.erb +1 -0
- data/test/rails/app/views/localite/template.en.html.erb +1 -0
- data/test/rails/config/boot.rb +110 -0
- data/test/rails/config/environment.rb +50 -0
- data/test/rails/config/environments/development.rb +17 -0
- data/test/rails/config/environments/production.rb +28 -0
- data/test/rails/config/environments/test.rb +28 -0
- data/test/rails/config/initializers/new_rails_defaults.rb +21 -0
- data/test/rails/config/initializers/session_store.rb +15 -0
- data/test/rails/config/locales/de.yml +4 -0
- data/test/rails/config/locales/en.yml +4 -0
- data/test/rails/config/routes.rb +13 -0
- data/test/rails/script/console +3 -0
- data/test/rails/script/server +3 -0
- data/test/rails/test/functional/localite_controller_test.rb +56 -0
- data/test/rails/test/test_helper.rb +38 -0
- data/test/test.rb +4 -1
- metadata +51 -11
- data/lib/localite/scope.rb +0 -140
- 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
|
data/lib/localite/template.rb
CHANGED
@@ -1,8 +1,16 @@
|
|
1
1
|
require "cgi"
|
2
2
|
|
3
3
|
class Localite::Template < String
|
4
|
-
def self.run(
|
5
|
-
new(template).run(
|
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(
|
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
|
-
|
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.
|
91
|
-
assert_equal "3 items", Template.
|
92
|
-
assert_equal "xyz", Template.
|
93
|
-
assert_equal "abc", Template.
|
94
|
-
assert_equal "3", Template.
|
95
|
-
assert_equal "3", Template.
|
96
|
-
assert_equal "3 Fixnums", Template.
|
97
|
-
assert_equal "3 Fixnums and 1 Float", Template.
|
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.
|
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.
|
125
|
-
assert_equal "a>c", Template.
|
126
|
-
assert_equal "> a>c", Template.
|
127
|
-
assert_equal "> a>c", Template.
|
128
|
+
assert_equal "a>c", Template.text("{*xyz*}", :xyz => "a>c")
|
129
|
+
assert_equal "a>c", Template.html("{*xyz*}", :xyz => "a>c")
|
130
|
+
assert_equal "> a>c", Template.text("> {*xyz*}", :xyz => "a>c")
|
131
|
+
assert_equal "> a>c", Template.html("> {*xyz*}", :xyz => "a>c")
|
128
132
|
end
|
129
133
|
|
130
134
|
def test_template_hash
|
131
|
-
assert_equal "a>c", Template.
|
132
|
-
assert_equal "a>c", Template.
|
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.
|
141
|
+
Template.text("{*abc*}", :xyz => "a>c")
|
138
142
|
}
|
139
143
|
end
|
140
144
|
end
|
data/lib/localite/tr.rb
ADDED
@@ -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
|