localite 0.3 → 0.5.6
Sign up to get free protection for your applications and to get access to all the features.
- 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,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 "<>", Localite::Format.html("text:<>")
|
28
|
+
assert_equal "<>", 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'>Ä Ö</p>", "<p lang='de'>Ä Ö</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'>Ä Ö</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'>Ä Ö</p>", "<p lang='de'>Ä Ö</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'>Ä Ö</p>", "<p lang='de'>Ä Ö</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'>Ä Ö</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
|
data/lib/localite/settings.rb
CHANGED
@@ -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
|
-
|
44
|
+
:en
|
6
45
|
end
|
7
46
|
|
8
47
|
#
|
9
48
|
# returns the current locale; defaults to the base locale
|
10
|
-
def
|
11
|
-
|
49
|
+
def current_locale
|
50
|
+
Thread.current[:"localite:locale"] || base
|
12
51
|
end
|
13
52
|
|
14
|
-
|
15
|
-
|
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
|
-
#
|
22
|
-
|
23
|
-
|
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
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|